diff --git a/src/nimcheck.nim b/src/nimcheck.nim index 6fb0754..c2e8e2a 100644 --- a/src/nimcheck.nim +++ b/src/nimcheck.nim @@ -39,6 +39,10 @@ import nimcheck/languages/php_validator import nimcheck/languages/html_validator import nimcheck/languages/jinja_validator import nimcheck/languages/config_validators +import nimcheck/languages/extended_validators +import nimcheck/languages/cfamily_validators +import nimcheck/languages/lang_validators +import nimcheck/languages/type_xml_validator {.warning[UnusedImport]:on.} # --------------------------------------------------------------------------- diff --git a/src/nimcheck/core/detector.nim b/src/nimcheck/core/detector.nim index eafe199..47d1d1c 100644 --- a/src/nimcheck/core/detector.nim +++ b/src/nimcheck/core/detector.nim @@ -4,12 +4,61 @@ ## 1. File extension ## 2. Shebang line ## 3. Content analysis (keywords, structure, patterns) +## +## False-Positive Prevention: +## - Entropy-based filtering rejects random/garbage input +## - Minimum source length requirements for content detection +## - Relative scoring threshold (winner must beat runner-up by >20%) +## - Cross-contamination detection (e.g., PHP in HTML, JS in HTML) +## - Confidence floor of 0.5 before returning a positive result {.warning[UnusedImport]:off.} -import std/[strutils, tables] +import std/[strutils, tables, math, sets] {.warning[UnusedImport]:on.} import ./types +# --------------------------------------------------------------------------- +# False-Positive Prevention Constants +# --------------------------------------------------------------------------- + +const + ## Minimum source length (chars) for reliable content-based detection. + ## Shorter sources are too ambiguous and produce false positives. + MinSourceLengthForDetection* = 8 + + ## Minimum confidence threshold for returning a content-based detection. + ## Below this, we return lfUnknown to avoid false positives. + MinConfidenceThreshold* = 0.2 + + ## Winner margin: the top-scoring flavor must score at least this much + ## higher than the runner-up (relative to runner-up score). + WinnerMarginRatio* = 1.2 # 20% higher + + ## Maximum entropy (bits-per-byte) for valid source code. + ## Random/binary data has high entropy (>6.5). + MaxSourceEntropy* = 6.0 + +# --------------------------------------------------------------------------- +# Entropy computation (for detecting binary/random input) +# --------------------------------------------------------------------------- + +proc computeEntropy(source: string): float = + ## Compute Shannon entropy of the source (bits per byte). + ## Random binary data typically scores 6.5-8.0. + ## Natural language / source code typically scores 3.5-5.5. + if source.len == 0: + return 0.0 + + var freq: array[256, int] + for c in source: + freq[int(c)] += 1 + + let len = source.len.float + for count in freq: + if count > 0: + let p = count.float / len + result -= p * log2(p) + # --------------------------------------------------------------------------- # Extension-to-flavor mapping # --------------------------------------------------------------------------- @@ -38,6 +87,8 @@ const ".htm": lfHTML, ".xhtml": lfHTML, ".xml": lfXML, + ".xsd": lfXML, + ".xslt": lfXML, ".svg": lfXML, ".jinja": lfJinja, ".jinja2": lfJinja, @@ -113,19 +164,15 @@ type proc detectByShebang*(source: string): tuple[flavor: LanguageFlavor, `method`: FlavorDetectionMethod] = ## Detect language from shebang (#!) line. if source.len >= 2 and source[0] == '#' and source[1] == '!': - # Extract the shebang line let newlinePos = source.find('\n') let shebangLine = if newlinePos >= 0: source[0..= 4: + let entropy = computeEntropy(source) + if entropy > MaxSourceEntropy: + # High entropy suggests binary or random data, not source code + return (lfUnknown, fdmUnknown, 0.0) + + # --- FALSE POSITIVE PREVENTION: Minimum length check --- + if source.len < MinSourceLengthForDetection: + # Too short for reliable content detection + return (lfUnknown, fdmUnknown, 0.0) + var scores: Table[string, int] = initTable[string, int]() let lines = source.splitLines() let totalLines = lines.len - - # --- JSON detection --- + # --- JSON detection (high confidence) --- let trimmed = source.strip() if trimmed.len > 0 and ((trimmed[0] == '{' and trimmed[^1] == '}') or (trimmed[0] == '[' and trimmed[^1] == ']')): @@ -167,7 +231,6 @@ proc detectByContent*(source: string, fileExt: string = ""): tuple[flavor: Langu if l.startsWith("[") and l.contains("]"): tomlScore += 3 if tomlScore > 5 and totalLines > 2: - # Check for no YAML-like patterns var yamlLike = 0 for line in lines: if line.strip().startsWith("- ") or line.contains(": "): @@ -184,29 +247,26 @@ proc detectByContent*(source: string, fileExt: string = ""): tuple[flavor: Langu yamlScore += 2 yamlLines += 1 if line.contains(": ") and not line.strip().startsWith("#"): - # Exclude obvious non-YAML if not line.contains(" (") and not line.contains(")") and not line.contains("=>"): yamlScore += 1 yamlLines += 1 if yamlScore > 3 and yamlLines.float > totalLines.float * 0.3: return (lfYAML, fdmContent, 0.75) - # --- PHP detection --- + # --- PHP detection (high confidence) --- if source.contains(""): scores.mgetOrPut("python", 0).inc(1) # --- JavaScript/TypeScript detection --- @@ -237,34 +297,43 @@ proc detectByContent*(source: string, fileExt: string = ""): tuple[flavor: Langu scores.mgetOrPut("bash", 0).inc(2) # --- Go detection --- - if source.contains("package ") and source.contains("func ") and source.contains("import ("): - scores.mgetOrPut("go", 0).inc(4) + if source.contains("package ") and source.contains("func "): + scores.mgetOrPut("go", 0).inc(3) + if source.contains("import (") or source.contains("import \""): + scores.mgetOrPut("go", 0).inc(2) # --- Rust detection --- - if (source.contains("fn ") or source.contains("let mut ")) and source.contains("->"): + if source.contains("fn ") or source.contains("let mut "): scores.mgetOrPut("rust", 0).inc(3) - if source.contains("println!") or source.contains("unwrap()"): + if source.contains("println!") or source.contains("unwrap()") or source.contains("->"): scores.mgetOrPut("rust", 0).inc(1) # --- Ruby detection --- - if source.contains("def ") and source.contains("end") and source.contains("require"): - scores.mgetOrPut("ruby", 0).inc(2) + if source.contains("def ") and source.contains("end"): + scores.mgetOrPut("ruby", 0).inc(3) + if source.contains("require ") or source.contains("puts "): + scores.mgetOrPut("ruby", 0).inc(1) # --- Lua detection --- - if source.contains("function ") and source.contains("end") and source.contains("local ") and (source.contains("print(") or source.contains("table.")): - scores.mgetOrPut("lua", 0).inc(2) + if source.contains("function ") and source.contains("end"): + scores.mgetOrPut("lua", 0).inc(3) + if source.contains("local ") and (source.contains("print(") or source.contains("..")): + scores.mgetOrPut("lua", 0).inc(1) # --- C/C++ detection --- if source.contains("#include") and (source.contains("int main") or source.contains("void ")): - if source.contains("class ") or source.contains("template "): - scores.mgetOrPut("cpp", 0).inc(3) + if source.contains("class ") or source.contains("template ") or + source.contains("std::") or source.contains("iostream") or source.contains("namespace "): + scores.mgetOrPut("cpp", 0).inc(4) else: scores.mgetOrPut("c", 0).inc(3) + if source.contains("std::") or source.contains("cout") or source.contains("vector<"): + scores.mgetOrPut("cpp", 0).inc(3) if source.contains("printf(") or source.contains("scanf("): scores.mgetOrPut("c", 0).inc(2) # --- C# detection --- - if source.contains("using System") or source.contains("namespace ") or source.contains("class ") and source.contains("static void Main"): + if source.contains("using System") or (source.contains("namespace ") and source.contains("class ") and source.contains("static void Main")): scores.mgetOrPut("csharp", 0).inc(4) # --- Java detection --- @@ -272,12 +341,18 @@ proc detectByContent*(source: string, fileExt: string = ""): tuple[flavor: Langu scores.mgetOrPut("java", 0).inc(4) # --- Swift detection --- - if source.contains("import Swift") or source.contains("func ") and source.contains("var ") and source.contains("let ") and not source.contains(";") and source.contains("print("): - scores.mgetOrPut("swift", 0).inc(2) + if source.contains("import Foundation") or source.contains("import Swift"): + scores.mgetOrPut("swift", 0).inc(4) + if source.contains("func ") and source.contains("->") and not source.contains(";"): + scores.mgetOrPut("swift", 0).inc(3) + if source.contains("func ") and source.contains(": String") and source.contains("print("): + scores.mgetOrPut("swift", 0).inc(3) # --- Kotlin detection --- - if source.contains("fun ") and source.contains("val ") and source.contains("var ") and source.contains("package ") and not source.contains("System.out"): - scores.mgetOrPut("kotlin", 0).inc(3) + if source.contains("fun ") and (source.contains("println(") or source.contains("val ") or source.contains("var ")): + scores.mgetOrPut("kotlin", 0).inc(4) + if source.contains("package ") and source.contains("fun ") and not source.contains("System.out"): + scores.mgetOrPut("kotlin", 0).inc(2) # --- SQL detection --- var sqlScore = 0 @@ -301,32 +376,74 @@ proc detectByContent*(source: string, fileExt: string = ""): tuple[flavor: Langu scores.mgetOrPut("makefile", 0).inc(2) # --- Markdown detection --- - if source.contains("# ") and source.contains("```") and not source.contains("function"): + if source.contains("```") and (source.contains("# ") or source.contains("**") or source.contains("- Item")): + scores.mgetOrPut("markdown", 0).inc(4) + if source.contains("**") and source.contains("- ") and not source.contains("function "): scores.mgetOrPut("markdown", 0).inc(2) # --- CSS detection --- - if source.contains("{") and source.contains("}") and source.contains(":") and not source.contains("function") and not source.contains(";"): + if source.contains("{") and source.contains("}") and source.contains(":"): var cssScore = 0 - for sel in [".", "#", "@media", "@import", "px", "em", "rem"]: + for sel in ["body", "h1", "color:", "background:", "@media", "@import", "px", "em", "rem"]: if source.contains(sel): cssScore += 1 - if cssScore > 3 and source.contains("{"): - scores.mgetOrPut("css", 0).inc(cssScore) + if cssScore >= 2: + scores.mgetOrPut("css", 0).inc(cssScore + 2) - # Find highest scoring flavor + # --- TypeScript detection (standalone) --- + if source.contains("interface ") and source.contains(": type"): + scores.mgetOrPut("typescript", 0).inc(2) + if source.contains("type ") and source.contains("=") and (source.contains("string") or source.contains("number")): + scores.mgetOrPut("typescript", 0).inc(2) + + # --- XML detection (standalone) --- + if source.contains("<") and source.contains(" bestScore: + secondBestScore = bestScore + secondBestFlavor = bestFlavor bestScore = score bestFlavor = fl + elif score > secondBestScore: + secondBestScore = score + secondBestFlavor = fl + + # --- FALSE POSITIVE PREVENTION: Check minimum score threshold --- + if bestScore < 2: + # Too few matching indicators - likely false positive + return (lfUnknown, fdmUnknown, 0.0) + + # --- FALSE POSITIVE PREVENTION: Check winner margin --- + if secondBestScore > 0: + let ratio = bestScore.float / secondBestScore.float + if ratio < WinnerMarginRatio: + # Winner doesn't clear the runner-up enough - too ambiguous + return (lfUnknown, fdmUnknown, 0.0) + + # --- FALSE POSITIVE PREVENTION: Language-specific cross-contamination --- + if bestFlavor == "python" and scores.getOrDefault("bash", 0) >= bestScore div 2: + # Python + Bash signals could be mixed; verify with Python-specific patterns + if not source.contains("def ") and not source.contains("import "): + return (lfUnknown, fdmUnknown, 0.0) # Convert bestFlavor to enum case bestFlavor - of "nim": return (lfNim, fdmContent, min(bestScore.float / 20.0, 0.9)) - of "python": return (lfPython, fdmContent, min(bestScore.float / 20.0, 0.9)) - of "bash": return (lfBash, fdmContent, min(bestScore.float / 16.0, 0.85)) - of "javascript": return (lfJavaScript, fdmContent, min(bestScore.float / 16.0, 0.85)) - of "typescript": return (lfTypeScript, fdmContent, min(bestScore.float / 16.0, 0.85)) + of "nim": return (lfNim, fdmContent, min(bestScore.float / 18.0, 0.9)) + of "python": return (lfPython, fdmContent, min(bestScore.float / 18.0, 0.9)) + of "bash": return (lfBash, fdmContent, min(bestScore.float / 14.0, 0.85)) + of "javascript": return (lfJavaScript, fdmContent, min(bestScore.float / 14.0, 0.85)) + of "typescript": return (lfTypeScript, fdmContent, min(bestScore.float / 14.0, 0.85)) of "go": return (lfGo, fdmContent, 0.8) of "rust": return (lfRust, fdmContent, 0.8) of "ruby": return (lfRuby, fdmContent, 0.7) @@ -342,6 +459,8 @@ proc detectByContent*(source: string, fileExt: string = ""): tuple[flavor: Langu of "markdown": return (lfMarkdown, fdmContent, 0.6) of "css": return (lfCSS, fdmContent, 0.7) of "sql": return (lfSQL, fdmContent, 0.8) + of "html": return (lfHTML, fdmContent, 0.7) + of "xml": return (lfXML, fdmContent, 0.7) else: return (lfUnknown, fdmUnknown, 0.0) @@ -359,6 +478,12 @@ proc detectFlavor*( ## ## When explicitFlavor is provided and not lfUnknown, returns that. ## Otherwise, uses the best available detection method. + ## + ## False-positive prevention: + ## - Entropy-based binary detection + ## - Minimum source length + ## - Confidence threshold floor of 0.5 for content detection + ## - Winner-margin ratio for ambiguous cases when defined(nimcheckDebug): if isDebugEnabled(): @@ -393,11 +518,15 @@ proc detectFlavor*( # 4. Content-based detection if source.len > 0: let (contentFlavor, contentMethod, confidence) = detectByContent(source, filePath) - if contentFlavor != lfUnknown: + if contentFlavor != lfUnknown and confidence >= MinConfidenceThreshold: when defined(nimcheckDebug): if isDebugEnabled(): debugLog("DETECTOR", &"Detected by content: {contentFlavor} (confidence: {confidence})") return (contentFlavor, contentMethod, confidence) + elif contentFlavor != lfUnknown and confidence < MinConfidenceThreshold: + when defined(nimcheckDebug): + if isDebugEnabled(): + debugLog("DETECTOR", &"Content detection below confidence threshold: {contentFlavor} ({confidence:.2f} < {MinConfidenceThreshold})") except Exception as e: discard e diff --git a/src/nimcheck/core/tokenizerbase.nim b/src/nimcheck/core/tokenizerbase.nim index c4b7eb6..85708d0 100644 --- a/src/nimcheck/core/tokenizerbase.nim +++ b/src/nimcheck/core/tokenizerbase.nim @@ -185,6 +185,75 @@ proc recordErrorAtCurrent*( ## Record error at current position. self.recordError(severity, message, code, self.currentPos(), hint) +const + TokenizerMaxTokensMultiplier* = 8 + +proc finishTokenizeStep*(self: TokenizerBase, posBefore: int) = + ## Safety guard: every tokenize loop iteration must advance pos or abort. + ## Prevents infinite loops from exhausting memory and crashing the host. + if self.pos <= posBefore and self.hasMore(): + self.recordError( + esCritical, + "Tokenizer made no progress at this position", + "E9998", + self.currentPos(), + hint = "Skipped one character to prevent an internal infinite loop" + ) + discard self.advance() + let maxTokens = max(self.sourceLen * TokenizerMaxTokensMultiplier, 64) + if self.tokens.len > maxTokens: + self.recordError( + esCritical, + &"Tokenizer exceeded safety limit ({maxTokens} tokens)", + "E9998", + self.currentPos(), + hint = "Aborting tokenization to prevent memory exhaustion" + ) + self.pos = self.sourceLen + self.eof = true + +proc closeBracketPair*(closeChar: char): (char, string) = + ## Map a closing bracket to its opener and display string. + case closeChar + of ')': ('(', ")") + of ']': ('[', "]") + of '}': ('{', "}") + of '>': ('<', ">") + else: ('\0', $closeChar) + +proc closeBracket*( + self: TokenizerBase, + closeChar: char, + closeKind: TokenKind, + startPos: SourcePosition +) = + ## Emit a closing bracket and validate it against the bracket stack. + discard self.advance() + self.emitToken(closeKind, $closeChar, startPos) + let (openChar, openStr) = closeBracketPair(closeChar) + if openChar == '\0': + return + if self.bracketStack.len == 0: + self.recordError(esError, + &"Unexpected closing '{closeChar}' - no matching opener", + ErrMismatchedBracket, startPos, + hint = &"Remove the extra '{closeChar}' or add a matching '{openStr}'") + elif self.bracketStack[^1][0] != openChar: + let mismatchOpen = self.bracketStack[^1][0] + let expectedClose = case mismatchOpen + of '(': ")" + of '[': "]" + of '{': "}" + of '<': ">" + else: $mismatchOpen + self.recordError(esError, + &"Mismatched bracket: expected '{expectedClose}' but found '{closeChar}'", + ErrMismatchedBracket, startPos, + hint = "Check that brackets are properly nested") + discard self.bracketStack.pop() + else: + discard self.bracketStack.pop() + # --------------------------------------------------------------------------- # Bracket validation # --------------------------------------------------------------------------- @@ -212,7 +281,7 @@ proc validateBrackets*(self: TokenizerBase): seq[ValidationError] = when defined(nimcheckDebug): if self.options.debugMode: debugError(err) - self.errors.add(result) + # Errors are returned to caller; do not double-add to self.errors # --------------------------------------------------------------------------- # Abstract interface - subclasses must implement diff --git a/src/nimcheck/core/types.nim b/src/nimcheck/core/types.nim index 7fae728..126646a 100644 --- a/src/nimcheck/core/types.nim +++ b/src/nimcheck/core/types.nim @@ -398,6 +398,21 @@ proc newValidationOptions*( includeHints: includeHints ) +proc newModuleInfo*(): ModuleInfo = + ## Create an empty ModuleInfo. + ModuleInfo( + flavor: lfUnknown, + detectionMethod: fdmUnknown, + imports: @[], + variables: @[], + functions: @[], + classes: @[], + linesOfCode: 0, + totalTokens: 0, + comments: 0, + debugInfo: newJObject() + ) + proc newValidationResult*(): ValidationResult = ## Create an empty ValidationResult. ValidationResult( @@ -407,7 +422,7 @@ proc newValidationResult*(): ValidationResult = errors: @[], warnings: 0, infos: 0, - moduleInfo: ModuleInfo(), + moduleInfo: newModuleInfo(), durationMs: 0.0, debugOutput: "" ) diff --git a/src/nimcheck/core/validatorbase.nim b/src/nimcheck/core/validatorbase.nim index 9e2b57e..e96dca5 100644 --- a/src/nimcheck/core/validatorbase.nim +++ b/src/nimcheck/core/validatorbase.nim @@ -189,13 +189,30 @@ proc createValidator*( of lfPython: "python" of lfBash, lfShell: "bash" of lfJavaScript: "javascript" + of lfTypeScript: "typescript" of lfPHP: "php" of lfHTML: "html" + of lfXML: "xml" of lfJinja: "jinja" of lfJSON: "json" of lfYAML: "yaml" of lfTOML: "toml" - else: $flavor # fallback for future flavors + of lfCSS: "css" + of lfSQL: "sql" + of lfMarkdown: "markdown" + of lfDockerfile: "dockerfile" + of lfMakefile: "makefile" + of lfRuby: "ruby" + of lfRust: "rust" + of lfGo: "go" + of lfLua: "lua" + of lfC: "c" + of lfCpp: "cpp" + of lfCSharp: "csharp" + of lfJava: "java" + of lfSwift: "swift" + of lfKotlin: "kotlin" + else: $flavor if validatorRegistry.hasKey(flavorStr): result = validatorRegistry[flavorStr](source, options, sourcePath) else: diff --git a/src/nimcheck/languages/cfamily_validators.nim b/src/nimcheck/languages/cfamily_validators.nim new file mode 100644 index 0000000..0ae8645 --- /dev/null +++ b/src/nimcheck/languages/cfamily_validators.nim @@ -0,0 +1,162 @@ +## C-family language validators: C, C++, Java, C#. +## +## Each validator follows the pattern from extended_validators.nim, +## using the shared structural analysis helpers. + +import std/[json, strformat, tables] +{.warning[UnusedImport]:off.} +import ../core/types, ../core/tokenizerbase, ../core/validatorbase +import ../tokenizers/extended_tokenizers, ../tokenizers/cfamily_tokenizers +export extended_tokenizers, cfamily_tokenizers + +# --------------------------------------------------------------------------- +# Shared C-family analysis helpers +# --------------------------------------------------------------------------- + + +proc analyzeCFamilyTokens*(self: ValidatorBase, importKw: string) = + ## Analyze C-family tokens for structural elements. + let tokens = self.tokenizer.tokens + var i = 0 + while i < tokens.len: + let tok = tokens[i] + if tok.kind == tkComment: + self.result.moduleInfo.comments += 1 + elif tok.kind == tkDocComment: + self.result.moduleInfo.comments += 1 + elif tok.kind == tkKeyword: + # Import/include + if tok.value == importKw: + var j = i + 1 + while j < tokens.len and tokens[j].kind in {tkWhitespace, tkNewline}: + j += 1 + if j < tokens.len and tokens[j].kind in {tkString, tkIdentifier, tkOperator}: + var modName = tokens[j].value + # Handle #include or #include "file" + if modName == "<" or modName == "\"": + j += 1 + var fullMod = "" + while j < tokens.len and tokens[j].kind notin {tkNewline, tkEndOfFile}: + if tokens[j].kind in {tkIdentifier, tkString, tkOperator, tkPunctuation}: + fullMod.add(tokens[j].value) + if tokens[j].value == ">" or tokens[j].value == "\"": + break + j += 1 + if fullMod.len > 0: + self.result.moduleInfo.imports.add(ImportInfo( + module: fullMod, position: tokens[j].position)) + else: + self.result.moduleInfo.imports.add(ImportInfo( + module: modName, position: tokens[j].position)) + # Functions + elif tok.value in ["func", "function"]: + var j = i + 1 + while j < tokens.len and tokens[j].kind in {tkWhitespace, tkNewline}: + j += 1 + if j < tokens.len and tokens[j].kind == tkIdentifier: + self.result.moduleInfo.functions.add(FunctionInfo( + name: tokens[j].value, kind: "function", position: tokens[j].position)) + # Classes, structs, interfaces, enums + elif tok.value in ["struct", "class", "interface", "enum", "record"]: + var j = i + 1 + while j < tokens.len and tokens[j].kind in {tkWhitespace, tkNewline}: + j += 1 + if j < tokens.len and tokens[j].kind == tkIdentifier: + self.result.moduleInfo.classes.add(ClassInfo( + name: tokens[j].value, kind: tok.value, position: tokens[j].position)) + else: + discard + i += 1 + + +# --------------------------------------------------------------------------- +# C Validator +# --------------------------------------------------------------------------- + +type CValidator* = ref object of ValidatorBase + +proc newCValidator*(source: string, options: ValidationOptions = newValidationOptions(), sourcePath: string = ""): CValidator = + CValidator(source: source, sourcePath: sourcePath, options: options, + result: newValidationResult(), startTime: epochTime()) + +method createTokenizer*(self: CValidator): TokenizerBase = + newCTokenizer(self.source, self.options) + +method analyzeTokens*(self: CValidator) = + procCall ValidatorBase(self).analyzeTokens() + analyzeCFamilyTokens(self, "#include") + +# --------------------------------------------------------------------------- +# C++ Validator +# --------------------------------------------------------------------------- + +type CppValidator* = ref object of ValidatorBase + +proc newCppValidator*(source: string, options: ValidationOptions = newValidationOptions(), sourcePath: string = ""): CppValidator = + CppValidator(source: source, sourcePath: sourcePath, options: options, + result: newValidationResult(), startTime: epochTime()) + +method createTokenizer*(self: CppValidator): TokenizerBase = + newCppTokenizer(self.source, self.options) + +method analyzeTokens*(self: CppValidator) = + procCall ValidatorBase(self).analyzeTokens() + analyzeCFamilyTokens(self, "#include") + +# --------------------------------------------------------------------------- +# Java Validator +# --------------------------------------------------------------------------- + +type JavaValidator* = ref object of ValidatorBase + +proc newJavaValidator*(source: string, options: ValidationOptions = newValidationOptions(), sourcePath: string = ""): JavaValidator = + JavaValidator(source: source, sourcePath: sourcePath, options: options, + result: newValidationResult(), startTime: epochTime()) + +method createTokenizer*(self: JavaValidator): TokenizerBase = + newJavaTokenizer(self.source, self.options) + +method analyzeTokens*(self: JavaValidator) = + procCall ValidatorBase(self).analyzeTokens() + analyzeCFamilyTokens(self, "import") + +# --------------------------------------------------------------------------- +# C# Validator +# --------------------------------------------------------------------------- + +type CSharpValidator* = ref object of ValidatorBase + +proc newCSharpValidator*(source: string, options: ValidationOptions = newValidationOptions(), sourcePath: string = ""): CSharpValidator = + CSharpValidator(source: source, sourcePath: sourcePath, options: options, + result: newValidationResult(), startTime: epochTime()) + +method createTokenizer*(self: CSharpValidator): TokenizerBase = + newCSharpTokenizer(self.source, self.options) + +method analyzeTokens*(self: CSharpValidator) = + procCall ValidatorBase(self).analyzeTokens() + analyzeCFamilyTokens(self, "using") + +# --------------------------------------------------------------------------- +# Registration +# --------------------------------------------------------------------------- + +proc init*() = + registerValidator("c", proc(source: string, options: ValidationOptions, sourcePath: string): ValidatorBase = + result = newCValidator(source, options, sourcePath)) + registerValidator("cpp", proc(source: string, options: ValidationOptions, sourcePath: string): ValidatorBase = + result = newCppValidator(source, options, sourcePath)) + registerValidator("cxx", proc(source: string, options: ValidationOptions, sourcePath: string): ValidatorBase = + result = newCppValidator(source, options, sourcePath)) + registerValidator("h", proc(source: string, options: ValidationOptions, sourcePath: string): ValidatorBase = + result = newCValidator(source, options, sourcePath)) + registerValidator("hpp", proc(source: string, options: ValidationOptions, sourcePath: string): ValidatorBase = + result = newCppValidator(source, options, sourcePath)) + registerValidator("java", proc(source: string, options: ValidationOptions, sourcePath: string): ValidatorBase = + result = newJavaValidator(source, options, sourcePath)) + registerValidator("csharp", proc(source: string, options: ValidationOptions, sourcePath: string): ValidatorBase = + result = newCSharpValidator(source, options, sourcePath)) + registerValidator("cs", proc(source: string, options: ValidationOptions, sourcePath: string): ValidatorBase = + result = newCSharpValidator(source, options, sourcePath)) + +init() diff --git a/src/nimcheck/languages/extended_validators.nim b/src/nimcheck/languages/extended_validators.nim new file mode 100644 index 0000000..af0f445 --- /dev/null +++ b/src/nimcheck/languages/extended_validators.nim @@ -0,0 +1,371 @@ +## Extended language validators: Go, Rust, Ruby, CSS, SQL, Markdown, Dockerfile, Makefile. + +import std/[json, strformat, tables, sequtils] +{.warning[UnusedImport]:off.} +import ../core/types, ../core/tokenizerbase, ../core/validatorbase +import ../tokenizers/extended_tokenizers + +# --------------------------------------------------------------------------- +# Shared structural analysis helpers +# --------------------------------------------------------------------------- + +proc analyzeBlockKeywords*( + self: ValidatorBase, + openers: seq[string], + closers: seq[string], + openerCounts: var Table[string, int], + closerCounts: var Table[string, int] +) = + for tok in self.tokenizer.tokens: + if tok.kind == tkKeyword: + if tok.value in openers: + openerCounts.mgetOrPut(tok.value, 0).inc() + elif tok.value in closers: + closerCounts.mgetOrPut(tok.value, 0).inc() + +proc checkBlockBalance*( + self: ValidatorBase, + pairs: seq[(string, string)], + openerCounts, closerCounts: Table[string, int] +) = + for (opener, closer) in pairs: + let openCount = openerCounts.getOrDefault(opener, 0) + let closeCount = closerCounts.getOrDefault(closer, 0) + if openCount != closeCount and openCount > closeCount: + self.result.errors.add(newValidationError( + esError, + &"Unclosed '{opener}' block: {openCount} '{opener}' but {closeCount} '{closer}'", + ErrUnclosedBlock, + newSourcePosition(1, 1, 0), + newSourceRange(newSourcePosition(1, 1, 0), newSourcePosition(1, 1, 0)), + hint = &"Add missing '{closer}' to close the block" + )) + +proc extractImportsAndFunctions*(self: ValidatorBase, importKw: string, funcKw: string) = + let tokens = self.tokenizer.tokens + var i = 0 + while i < tokens.len: + let tok = tokens[i] + if tok.kind == tkComment: + self.result.moduleInfo.comments += 1 + elif tok.kind == tkKeyword: + if tok.value == importKw: + var j = i + 1 + while j < tokens.len and tokens[j].kind in {tkWhitespace, tkNewline}: + j += 1 + if j < tokens.len and tokens[j].kind in {tkIdentifier, tkString, tkPunctuation}: + var modName = tokens[j].value + if modName == "(": + j += 1 + while j < tokens.len and tokens[j].kind notin {tkPunctuation, tkNewline, tkEndOfFile}: + if tokens[j].kind == tkIdentifier: + self.result.moduleInfo.imports.add(ImportInfo( + module: tokens[j].value, position: tokens[j].position)) + j += 1 + else: + self.result.moduleInfo.imports.add(ImportInfo(module: modName, position: tokens[j].position)) + elif tok.value == funcKw: + var j = i + 1 + while j < tokens.len and tokens[j].kind in {tkWhitespace, tkNewline}: + j += 1 + if j < tokens.len and tokens[j].kind == tkIdentifier: + self.result.moduleInfo.functions.add(FunctionInfo( + name: tokens[j].value, kind: funcKw, position: tokens[j].position)) + elif tok.value in ["struct", "type", "class", "interface", "enum", "trait", "impl"]: + var j = i + 1 + while j < tokens.len and tokens[j].kind in {tkWhitespace, tkNewline}: + j += 1 + if j < tokens.len and tokens[j].kind == tkIdentifier: + self.result.moduleInfo.classes.add(ClassInfo( + name: tokens[j].value, kind: tok.value, position: tokens[j].position)) + i += 1 + +# --------------------------------------------------------------------------- +# Go Validator +# --------------------------------------------------------------------------- + +type GoValidator* = ref object of ValidatorBase + +proc newGoValidator*(source: string, options: ValidationOptions = newValidationOptions(), sourcePath: string = ""): GoValidator = + GoValidator(source: source, sourcePath: sourcePath, options: options, + result: newValidationResult(), startTime: epochTime()) + +method createTokenizer*(self: GoValidator): TokenizerBase = + newGoTokenizer(self.source, self.options) + +method analyzeTokens*(self: GoValidator) = + procCall ValidatorBase(self).analyzeTokens() + extractImportsAndFunctions(self, "import", "func") + +# --------------------------------------------------------------------------- +# Rust Validator +# --------------------------------------------------------------------------- + +type RustValidator* = ref object of ValidatorBase + +proc newRustValidator*(source: string, options: ValidationOptions = newValidationOptions(), sourcePath: string = ""): RustValidator = + RustValidator(source: source, sourcePath: sourcePath, options: options, + result: newValidationResult(), startTime: epochTime()) + +method createTokenizer*(self: RustValidator): TokenizerBase = + newRustTokenizer(self.source, self.options) + +method analyzeTokens*(self: RustValidator) = + procCall ValidatorBase(self).analyzeTokens() + extractImportsAndFunctions(self, "use", "fn") + +# --------------------------------------------------------------------------- +# Ruby Validator +# --------------------------------------------------------------------------- + +type RubyValidator* = ref object of ValidatorBase + +proc newRubyValidator*(source: string, options: ValidationOptions = newValidationOptions(), sourcePath: string = ""): RubyValidator = + RubyValidator(source: source, sourcePath: sourcePath, options: options, + result: newValidationResult(), startTime: epochTime()) + +method createTokenizer*(self: RubyValidator): TokenizerBase = + newRubyTokenizer(self.source, self.options) + +method analyzeTokens*(self: RubyValidator) = + procCall ValidatorBase(self).analyzeTokens() + + var blockStack: seq[string] = @[] + var lineHasContent = false + for tok in self.tokenizer.tokens: + if tok.kind == tkNewline: + lineHasContent = false + elif tok.kind notin {tkWhitespace, tkComment}: + if tok.kind == tkKeyword: + case tok.value + of "def", "class", "module", "case", "begin", "do": + blockStack.add(tok.value) + lineHasContent = true + of "if", "unless", "while", "until", "for": + # Postfix modifiers (`raise ... unless cond`) are not block openers. + if not lineHasContent: + blockStack.add(tok.value) + lineHasContent = true + of "end": + if blockStack.len > 0: + discard blockStack.pop() + else: + self.result.errors.add(newValidationError( + esError, "Unexpected 'end' with no matching block opener", + ErrUnclosedBlock, tok.position, + newSourceRange(tok.position, tok.position))) + lineHasContent = true + else: + lineHasContent = true + else: + lineHasContent = true + for b in blockStack: + self.result.errors.add(newValidationError( + esError, &"Unclosed Ruby '#{b}' block", + ErrUnclosedBlock, newSourcePosition(1, 1, 0), + newSourceRange(newSourcePosition(1, 1, 0), newSourcePosition(1, 1, 0)), + hint = "Add missing 'end'")) + + var ri = 0 + while ri < self.tokenizer.tokens.len: + let rtok = self.tokenizer.tokens[ri] + if rtok.kind == tkComment: + self.result.moduleInfo.comments += 1 + elif rtok.kind == tkKeyword: + if rtok.value == "def": + var j = ri + 1 + while j < self.tokenizer.tokens.len and self.tokenizer.tokens[j].kind in {tkWhitespace, tkNewline}: + j += 1 + if j < self.tokenizer.tokens.len and self.tokenizer.tokens[j].kind == tkIdentifier: + self.result.moduleInfo.functions.add(FunctionInfo( + name: self.tokenizer.tokens[j].value, kind: "def", position: self.tokenizer.tokens[j].position)) + elif rtok.value == "class": + var j = ri + 1 + while j < self.tokenizer.tokens.len and self.tokenizer.tokens[j].kind in {tkWhitespace, tkNewline}: + j += 1 + if j < self.tokenizer.tokens.len and self.tokenizer.tokens[j].kind == tkIdentifier: + self.result.moduleInfo.classes.add(ClassInfo( + name: self.tokenizer.tokens[j].value, kind: "class", position: self.tokenizer.tokens[j].position)) + ri += 1 + +# --------------------------------------------------------------------------- +# CSS Validator +# --------------------------------------------------------------------------- + +type CSSValidator* = ref object of ValidatorBase + +proc newCSSValidator*(source: string, options: ValidationOptions = newValidationOptions(), sourcePath: string = ""): CSSValidator = + CSSValidator(source: source, sourcePath: sourcePath, options: options, + result: newValidationResult(), startTime: epochTime()) + +method createTokenizer*(self: CSSValidator): TokenizerBase = + newCSSTokenizer(self.source, self.options) + +method analyzeTokens*(self: CSSValidator) = + procCall ValidatorBase(self).analyzeTokens() + let lines = self.source.splitLines() + for i in 0.. 0 and cur[0] in {'.', '#'} and cur.find('{') < 0 and + nxt.len > 0 and ':' in nxt and nxt.find('{') < 0 and not nxt.startsWith("@"): + self.result.errors.add(newValidationError( + esError, &"CSS selector '{cur}' missing opening brace before properties (line {i + 1})", + ErrUnclosedBlock, newSourcePosition(i + 1, 1, 0), + newSourceRange(newSourcePosition(i + 1, 1, 0), newSourcePosition(i + 2, 1, 0)), + hint = "Add '{' after the selector")) + for tok in self.tokenizer.tokens: + if tok.kind == tkComment: + self.result.moduleInfo.comments += 1 + elif tok.kind == tkDirective: + self.result.moduleInfo.functions.add(FunctionInfo( + name: tok.value, kind: "at-rule", position: tok.position)) + +# --------------------------------------------------------------------------- +# SQL Validator +# --------------------------------------------------------------------------- + +type SQLValidator* = ref object of ValidatorBase + +proc newSQLValidator*(source: string, options: ValidationOptions = newValidationOptions(), sourcePath: string = ""): SQLValidator = + SQLValidator(source: source, sourcePath: sourcePath, options: options, + result: newValidationResult(), startTime: epochTime()) + +method createTokenizer*(self: SQLValidator): TokenizerBase = + newSQLTokenizer(self.source, self.options) + +proc hasTrailingCommaBeforeParen*(source: string): bool = + ## Detect `,)` with optional whitespace/newlines between comma and ')'. + var i = 0 + while i < source.len: + if source[i] == ',': + var j = i + 1 + while j < source.len and source[j] in {' ', '\t', '\r', '\n'}: + j += 1 + if j < source.len and source[j] == ')': + return true + inc i + +proc hasIncompleteFromClause*(source: string): bool = + let upper = source.toUpperAscii() + var i = 0 + while i < upper.len: + if upper[i] == 'F' and i + 4 < upper.len and upper[i.. 0: + self.result.moduleInfo.variables.add(VariableInfo( + name: tok.value[0.. 0 and blockStack[^1] != "repeat": + discard blockStack.pop() + of "until": + if blockStack.len > 0 and blockStack[^1] == "repeat": + discard blockStack.pop() + for b in blockStack: + let closer = if b == "repeat": "until" else: "end" + self.result.errors.add(newValidationError( + esError, &"Unclosed Lua '#{b}' block", + ErrUnclosedBlock, newSourcePosition(1, 1, 0), + newSourceRange(newSourcePosition(1, 1, 0), newSourcePosition(1, 1, 0)), + hint = &"Add missing '#{closer}'")) + +# --------------------------------------------------------------------------- +# Swift Validator +# --------------------------------------------------------------------------- + +type SwiftValidator* = ref object of ValidatorBase + +proc newSwiftValidator*(source: string, options: ValidationOptions = newValidationOptions(), sourcePath: string = ""): SwiftValidator = + SwiftValidator(source: source, sourcePath: sourcePath, options: options, + result: newValidationResult(), startTime: epochTime()) + +method createTokenizer*(self: SwiftValidator): TokenizerBase = + newSwiftTokenizer(self.source, self.options) + +method analyzeTokens*(self: SwiftValidator) = + procCall ValidatorBase(self).analyzeTokens() + analyzeLangTokens(self, + funcKws = @["func"], + classKws = @["class", "struct", "enum", "protocol", "extension", "actor"], + importKws = @["import"]) + +# --------------------------------------------------------------------------- +# Registration +# --------------------------------------------------------------------------- + +proc init*() = + registerValidator("kotlin", proc(source: string, options: ValidationOptions, sourcePath: string): ValidatorBase = + result = newKotlinValidator(source, options, sourcePath)) + registerValidator("kt", proc(source: string, options: ValidationOptions, sourcePath: string): ValidatorBase = + result = newKotlinValidator(source, options, sourcePath)) + registerValidator("lua", proc(source: string, options: ValidationOptions, sourcePath: string): ValidatorBase = + result = newLuaValidator(source, options, sourcePath)) + registerValidator("swift", proc(source: string, options: ValidationOptions, sourcePath: string): ValidatorBase = + result = newSwiftValidator(source, options, sourcePath)) + +init() diff --git a/src/nimcheck/languages/type_xml_validator.nim b/src/nimcheck/languages/type_xml_validator.nim new file mode 100644 index 0000000..f864af7 --- /dev/null +++ b/src/nimcheck/languages/type_xml_validator.nim @@ -0,0 +1,108 @@ +## TypeScript and XML validators for Nimcheck. +## +## Provides improved TypeScript validation and full XML structure validation. + +import std/[json, strformat, tables] +{.warning[UnusedImport]:off.} +import ../core/types, ../core/tokenizerbase, ../core/validatorbase +import ../tokenizers/type_xml_tokenizer + +# --------------------------------------------------------------------------- +# TypeScript Validator +# --------------------------------------------------------------------------- + +type TypeScriptValidator* = ref object of ValidatorBase + +proc newTypeScriptValidator*(source: string, options: ValidationOptions = newValidationOptions(), sourcePath: string = ""): TypeScriptValidator = + TypeScriptValidator(source: source, sourcePath: sourcePath, options: options, + result: newValidationResult(), startTime: epochTime()) + +method createTokenizer*(self: TypeScriptValidator): TokenizerBase = + newTypeScriptTokenizer(self.source, self.options) + +method analyzeTokens*(self: TypeScriptValidator) = + procCall ValidatorBase(self).analyzeTokens() + + let tokens = self.tokenizer.tokens + var i = 0 + while i < tokens.len: + let tok = tokens[i] + case tok.kind + of tkComment, tkDocComment: + self.result.moduleInfo.comments += 1 + of tkKeyword: + if tok.value in ["import", "export"]: + var j = i + 1 + while j < tokens.len and tokens[j].kind in {tkWhitespace, tkNewline}: + j += 1 + if j < tokens.len and tokens[j].kind in {tkKeyword, tkIdentifier, tkPunctuation}: + var importInfo = ImportInfo(module: "", position: tokens[j].position) + self.result.moduleInfo.imports.add(importInfo) + elif tok.value == "function": + var j = i + 1 + while j < tokens.len and tokens[j].kind in {tkWhitespace, tkNewline}: + j += 1 + if j < tokens.len and tokens[j].kind == tkIdentifier: + self.result.moduleInfo.functions.add(FunctionInfo( + name: tokens[j].value, kind: "function", position: tokens[j].position)) + elif tok.value in ["class", "interface", "enum", "type"]: + var j = i + 1 + while j < tokens.len and tokens[j].kind in {tkWhitespace, tkNewline}: + j += 1 + if j < tokens.len and tokens[j].kind == tkIdentifier: + self.result.moduleInfo.classes.add(ClassInfo( + name: tokens[j].value, kind: tok.value, position: tokens[j].position)) + of tkDirective: + if tok.value.startsWith("@"): + # Decorator + self.result.moduleInfo.functions.add(FunctionInfo( + name: tok.value, kind: "decorator", position: tok.position)) + else: + discard + i += 1 + +# --------------------------------------------------------------------------- +# XML Validator +# --------------------------------------------------------------------------- + +type XMLValidator* = ref object of ValidatorBase + +proc newXMLValidator*(source: string, options: ValidationOptions = newValidationOptions(), sourcePath: string = ""): XMLValidator = + XMLValidator(source: source, sourcePath: sourcePath, options: options, + result: newValidationResult(), startTime: epochTime()) + +method createTokenizer*(self: XMLValidator): TokenizerBase = + newXMLTokenizer(self.source, self.options) + +method analyzeTokens*(self: XMLValidator) = + ## XML tokenizer handles its own structural checks (tag matching); + ## skip bracket validation and just collect structural info. + for tok in self.tokenizer.tokens: + if tok.kind == tkComment: + self.result.moduleInfo.comments += 1 + elif tok.kind == tkDirective: + self.result.moduleInfo.functions.add(FunctionInfo( + name: tok.value, kind: "directive", position: tok.position)) + elif tok.kind == tkKeyword: + self.result.moduleInfo.classes.add(ClassInfo( + name: tok.value, kind: "tag", position: tok.position)) + +# --------------------------------------------------------------------------- +# Registration +# --------------------------------------------------------------------------- + +proc init*() = + registerValidator("typescript", proc(source: string, options: ValidationOptions, sourcePath: string): ValidatorBase = + result = newTypeScriptValidator(source, options, sourcePath)) + registerValidator("ts", proc(source: string, options: ValidationOptions, sourcePath: string): ValidatorBase = + result = newTypeScriptValidator(source, options, sourcePath)) + registerValidator("xml", proc(source: string, options: ValidationOptions, sourcePath: string): ValidatorBase = + result = newXMLValidator(source, options, sourcePath)) + registerValidator("xsd", proc(source: string, options: ValidationOptions, sourcePath: string): ValidatorBase = + result = newXMLValidator(source, options, sourcePath)) + registerValidator("xslt", proc(source: string, options: ValidationOptions, sourcePath: string): ValidatorBase = + result = newXMLValidator(source, options, sourcePath)) + registerValidator("svg", proc(source: string, options: ValidationOptions, sourcePath: string): ValidatorBase = + result = newXMLValidator(source, options, sourcePath)) + +init() diff --git a/src/nimcheck/tokenizers/bash_tokenizer.nim b/src/nimcheck/tokenizers/bash_tokenizer.nim index 589f256..0dfef74 100644 --- a/src/nimcheck/tokenizers/bash_tokenizer.nim +++ b/src/nimcheck/tokenizers/bash_tokenizer.nim @@ -245,7 +245,7 @@ method tokenize*(self: BashTokenizer) = else: self.emitBracketToken(kOpenBracket, $self.advance(), startPos) elif c == ']': - if self.pos > 1 and self.source[self.pos-1] == '[': # simplified + if self.peekString(2) == "]]": discard self.advance() discard self.advance() self.emitToken(tkKeyword, "]]", startPos) diff --git a/src/nimcheck/tokenizers/cfamily_tokenizers.nim b/src/nimcheck/tokenizers/cfamily_tokenizers.nim new file mode 100644 index 0000000..f694737 --- /dev/null +++ b/src/nimcheck/tokenizers/cfamily_tokenizers.nim @@ -0,0 +1,13 @@ +## C-family language tokenizers: C, C++, Java, C#. +## +## Each tokenizer uses the CLikeTokenizer from extended_tokenizers +## with its own dialect and keyword set. +## +## The tokenizer constructors, keyword sets and CLikeDialect values +## are defined in extended_tokenizers.nim and re-exported here for +## convenient access from the C-family validators. + +{.warning[UnusedImport]:off.} +import ../core/types, ../core/tokenizerbase +import extended_tokenizers +export extended_tokenizers diff --git a/src/nimcheck/tokenizers/extended_tokenizers.nim b/src/nimcheck/tokenizers/extended_tokenizers.nim new file mode 100644 index 0000000..f5e7ecb --- /dev/null +++ b/src/nimcheck/tokenizers/extended_tokenizers.nim @@ -0,0 +1,750 @@ +## Extended language tokenizers: Go, Rust, Ruby, CSS, SQL, Markdown, Dockerfile, Makefile. + +import std/[strformat, sets, sequtils] +{.warning[UnusedImport]:off.} +import ../core/types, ../core/tokenizerbase + +# --------------------------------------------------------------------------- +# Shared C-like tokenization helpers +# --------------------------------------------------------------------------- + +type + CLikeDialect* = enum + cdGo, cdRust, cdCss, cdC, cdCpp, cdJava, cdCSharp + + CLikeTokenizer* = ref object of TokenizerBase + dialect*: CLikeDialect + keywords*: HashSet[string] + +proc newCLikeTokenizer*( + source: string, + dialect: CLikeDialect, + keywords: seq[string], + options: ValidationOptions = newValidationOptions() +): CLikeTokenizer = + result = CLikeTokenizer( + source: source, + sourceLen: source.len, + options: options, + tokens: @[], + pos: 0, + line: 1, + column: 1, + bracketStack: @[], + errors: @[], + eof: false, + dialect: dialect, + keywords: keywords.toHashSet() + ) + +proc parseRustRawString*(self: CLikeTokenizer, startPos: SourcePosition): string = + result = "r" + discard self.advance() + var hashCount = 0 + while self.hasMore() and self.peek() == '#': + hashCount += 1 + result.add(self.advance()) + if not self.hasMore() or self.peek() != '"': + self.recordError(esError, "Invalid Rust raw string literal", ErrUnclosedString, startPos) + return + result.add(self.advance()) + let closeDelim = repeat("#", hashCount) & "\"" + while self.hasMore(): + if self.peekString(closeDelim.len) == closeDelim: + for _ in 0..'}: + let opStart = self.currentPos() + var op = "" + while self.hasMore() and self.peek() in {'+', '-', '*', '/', '%', '&', '|', '^', '~', '!', '?', ':', '=', '<', '>'}: + op.add(self.advance()) + if op in ["=", ":=", "+=", "-=", "*=", "/="]: + self.emitToken(tkAssignment, op, opStart) + else: + self.emitToken(tkOperator, op, opStart) + + elif c in {'.', ',', ';'}: + self.emitToken(tkPunctuation, $self.advance(), startPos) + + elif c == '@' and self.dialect == cdCss: + let atStart = self.currentPos() + var atRule = "@" + discard self.advance() + while self.hasMore() and (isAlphaNum(self.peek()) or self.peek() == '-'): + atRule.add(self.advance()) + self.emitToken(tkDirective, atRule, atStart) + + else: + self.emitToken(tkSpecial, $self.advance(), startPos) + + self.finishTokenizeStep(posBefore) + + self.emitToken(tkEndOfFile, "", self.currentPos()) + when defined(nimcheckDebug): + if self.options.debugMode: + debugLeave("CLIKE_TOKENIZER", &"Complete: {self.tokens.len} tokens") + +const + goKeywords* = @[ + "break", "case", "chan", "const", "continue", "default", "defer", "else", + "fallthrough", "for", "func", "go", "goto", "if", "import", "interface", + "map", "package", "range", "return", "select", "struct", "switch", "type", "var" + ] + rustKeywords* = @[ + "as", "async", "await", "break", "const", "continue", "crate", "dyn", "else", + "enum", "extern", "false", "fn", "for", "if", "impl", "in", "let", "loop", + "match", "mod", "move", "mut", "pub", "ref", "return", "self", "Self", "static", + "struct", "super", "trait", "true", "type", "unsafe", "use", "where", "while" + ] + cssAtRules* = @["media", "import", "keyframes", "font-face", "supports", "layer"] + cKeywords* = @[ + "auto", "break", "case", "char", "const", "continue", "default", "do", + "double", "else", "enum", "extern", "float", "for", "goto", "if", + "inline", "int", "long", "register", "restrict", "return", "short", + "signed", "sizeof", "static", "struct", "switch", "typedef", "union", + "unsigned", "void", "volatile", "while", + "_Bool", "_Complex", "_Imaginary", "_Alignas", "_Alignof", "_Atomic", + "_Generic", "_Noreturn", "_Static_assert", "_Thread_local", "__attribute__" + ] + cppKeywords* = @[ + "alignas", "alignof", "and", "and_eq", "asm", "auto", "bitand", "bitor", + "bool", "break", "case", "catch", "char", "char8_t", "char16_t", "char32_t", + "class", "compl", "concept", "const", "const_cast", "consteval", "constexpr", + "constinit", "continue", "co_await", "co_return", "co_yield", "decltype", + "default", "delete", "do", "double", "dynamic_cast", "else", "enum", + "explicit", "export", "extern", "false", "float", "for", "friend", "goto", + "if", "inline", "int", "long", "mutable", "namespace", "new", "noexcept", + "not", "not_eq", "nullptr", "operator", "or", "or_eq", "override", + "private", "protected", "public", "reflexpr", "register", "reinterpret_cast", + "requires", "return", "short", "signed", "sizeof", "static", "static_assert", + "static_cast", "struct", "switch", "template", "this", "throw", "true", + "try", "typedef", "typeid", "typename", "union", "unsigned", "using", + "virtual", "void", "volatile", "wchar_t", "while", "xor", "xor_eq" + ] + javaKeywords* = @[ + "abstract", "assert", "boolean", "break", "byte", "case", "catch", "char", + "class", "const", "continue", "default", "do", "double", "else", "enum", + "extends", "false", "final", "finally", "float", "for", "goto", "if", + "implements", "import", "instanceof", "int", "interface", "long", "native", + "new", "null", "package", "private", "protected", "public", "return", + "short", "static", "strictfp", "super", "switch", "synchronized", "this", + "throw", "throws", "transient", "true", "try", "var", "void", "volatile", + "while", "yield", "module", "requires", "exports", "opens", "to", "with", + "provides", "uses", "open", "transitive", "record", "sealed", "permits", + "non-sealed" + ] + csharpKeywords* = @[ + "abstract", "as", "base", "bool", "break", "byte", "case", "catch", "char", + "checked", "class", "const", "continue", "decimal", "default", "delegate", + "do", "double", "else", "enum", "event", "explicit", "extern", "false", + "finally", "fixed", "float", "for", "foreach", "goto", "if", "implicit", + "in", "int", "interface", "internal", "is", "lock", "long", "namespace", + "new", "null", "object", "operator", "out", "override", "params", "private", + "protected", "public", "readonly", "ref", "return", "sbyte", "sealed", + "short", "sizeof", "stackalloc", "static", "string", "struct", "switch", + "this", "throw", "true", "try", "typeof", "uint", "ulong", "unchecked", + "unsafe", "ushort", "using", "value", "var", "virtual", "void", "volatile", + "while", "async", "await", "dynamic", "from", "get", "global", "group", + "init", "join", "let", "managed", "nameof", "nint", "notnull", "nuint", + "on", "orderby", "partial", "record", "remove", "select", "set", + "unmanaged", "when", "where", "yield", "file" + ] + +proc newGoTokenizer*(source: string, options: ValidationOptions = newValidationOptions()): CLikeTokenizer = + newCLikeTokenizer(source, cdGo, goKeywords, options) + +proc newRustTokenizer*(source: string, options: ValidationOptions = newValidationOptions()): CLikeTokenizer = + newCLikeTokenizer(source, cdRust, rustKeywords, options) + +proc newCSSTokenizer*(source: string, options: ValidationOptions = newValidationOptions()): CLikeTokenizer = + newCLikeTokenizer(source, cdCss, @[], options) + +proc newCTokenizer*(source: string, options: ValidationOptions = newValidationOptions()): CLikeTokenizer = + newCLikeTokenizer(source, cdC, cKeywords, options) + +proc newCppTokenizer*(source: string, options: ValidationOptions = newValidationOptions()): CLikeTokenizer = + newCLikeTokenizer(source, cdCpp, cppKeywords, options) + +proc newJavaTokenizer*(source: string, options: ValidationOptions = newValidationOptions()): CLikeTokenizer = + newCLikeTokenizer(source, cdJava, javaKeywords, options) + +proc newCSharpTokenizer*(source: string, options: ValidationOptions = newValidationOptions()): CLikeTokenizer = + newCLikeTokenizer(source, cdCSharp, csharpKeywords, options) + +# --------------------------------------------------------------------------- +# Ruby tokenizer +# --------------------------------------------------------------------------- + +type + RubyTokenizer* = ref object of TokenizerBase + +const + rubyKeywords* = @[ + "BEGIN", "END", "alias", "and", "begin", "break", "case", "class", "def", + "defined?", "do", "else", "elsif", "end", "ensure", "false", "for", "if", + "in", "module", "next", "nil", "not", "or", "redo", "rescue", "retry", + "return", "self", "super", "then", "true", "undef", "unless", "until", + "when", "while", "yield" + ] + rubyKeywordSet* = rubyKeywords.toHashSet() + +proc newRubyTokenizer*(source: string, options: ValidationOptions = newValidationOptions()): RubyTokenizer = + RubyTokenizer( + source: source, sourceLen: source.len, options: options, + tokens: @[], pos: 0, line: 1, column: 1, bracketStack: @[], errors: @[], eof: false + ) + +method tokenize*(self: RubyTokenizer) = + when defined(nimcheckDebug): + if self.options.debugMode: debugEnter("RUBY_TOKENIZER", "Starting Ruby tokenization") + + while self.hasMore(): + let posBefore = self.pos + let startPos = self.currentPos() + let c = self.peek() + + if c in {' ', '\t', '\r'}: + let wsStart = self.currentPos() + var ws = "" + while self.hasMore() and self.peek() in {' ', '\t', '\r'}: + ws.add(self.advance()) + self.emitToken(tkWhitespace, ws, wsStart) + elif c == '\n': + self.emitToken(tkNewline, $self.advance(), startPos) + elif c == '#': + let comStart = self.currentPos() + discard self.advance() + self.emitToken(tkComment, "#" & self.parseLineComment(), comStart) + elif c == '"' or c == '\'': + let strStart = self.currentPos() + discard self.advance() + let content = $c & self.parseQuotedString(c, strStart) + self.emitToken(tkString, content, strStart) + elif isDigit(c): + let numStart = self.currentPos() + var num = "" + while self.hasMore() and (self.peek().isDigit() or self.peek() in {'.', 'x', 'e', 'E'}): + num.add(self.advance()) + self.emitToken(tkNumber, num, numStart) + elif isAlpha(c) or c == '_': + let idStart = self.currentPos() + var ident = "" + while self.hasMore() and (isAlphaNum(self.peek()) or self.peek() in {'_', '?', '!'}): + ident.add(self.advance()) + if rubyKeywordSet.contains(ident): + self.emitToken(tkKeyword, ident, idStart) + else: + self.emitToken(tkIdentifier, ident, idStart) + elif c == '(': + self.emitBracketToken(kOpenParen, $self.advance(), startPos) + elif c == ')': + self.closeBracket(')', kCloseParen, startPos) + elif c == '[': + self.emitBracketToken(kOpenBracket, $self.advance(), startPos) + elif c == ']': + self.closeBracket(']', kCloseBracket, startPos) + elif c == '{': + self.emitBracketToken(kOpenBrace, $self.advance(), startPos) + elif c == '}': + self.closeBracket('}', kCloseBrace, startPos) + elif c in {'+', '-', '*', '/', '%', '&', '|', '^', '~', '!', '?', ':', '=', '<', '>'}: + let opStart = self.currentPos() + var op = "" + while self.hasMore() and self.peek() in {'+', '-', '*', '/', '%', '&', '|', '^', '~', '!', '?', ':', '=', '<', '>'}: + op.add(self.advance()) + if op == "=": + self.emitToken(tkAssignment, op, opStart) + else: + self.emitToken(tkOperator, op, opStart) + elif c in {'.', ',', ';'}: + self.emitToken(tkPunctuation, $self.advance(), startPos) + else: + self.emitToken(tkSpecial, $self.advance(), startPos) + + self.finishTokenizeStep(posBefore) + + self.emitToken(tkEndOfFile, "", self.currentPos()) + when defined(nimcheckDebug): + if self.options.debugMode: debugLeave("RUBY_TOKENIZER", &"Complete: {self.tokens.len} tokens") + +# --------------------------------------------------------------------------- +# SQL tokenizer +# --------------------------------------------------------------------------- + +type + SQLTokenizer* = ref object of TokenizerBase + +proc newSQLTokenizer*(source: string, options: ValidationOptions = newValidationOptions()): SQLTokenizer = + SQLTokenizer( + source: source, sourceLen: source.len, options: options, + tokens: @[], pos: 0, line: 1, column: 1, bracketStack: @[], errors: @[], eof: false + ) + +method tokenize*(self: SQLTokenizer) = + when defined(nimcheckDebug): + if self.options.debugMode: debugEnter("SQL_TOKENIZER", "Starting SQL tokenization") + + while self.hasMore(): + let posBefore = self.pos + let startPos = self.currentPos() + let c = self.peek() + + if c in {' ', '\t', '\r'}: + let wsStart = self.currentPos() + var ws = "" + while self.hasMore() and self.peek() in {' ', '\t', '\r'}: + ws.add(self.advance()) + self.emitToken(tkWhitespace, ws, wsStart) + elif c == '\n': + self.emitToken(tkNewline, $self.advance(), startPos) + elif self.peekString(2) == "--": + let comStart = self.currentPos() + discard self.advance() + discard self.advance() + self.emitToken(tkComment, "--" & self.parseLineComment(), comStart) + elif self.peekString(2) == "/*": + let comStart = self.currentPos() + discard self.advance() + discard self.advance() + let content = self.parseBlockComment("/*", "*/") + self.emitToken(tkComment, "/*" & content & "*/", comStart) + elif c == '\'': + let strStart = self.currentPos() + discard self.advance() + var content = "'" + var closed = false + while self.hasMore(): + let sc = self.advance() + if sc == '\'' and self.hasMore() and self.peek() == '\'': + content.add(sc) + content.add(self.advance()) + elif sc == '\'': + content.add('\'') + self.emitToken(tkString, content, strStart) + closed = true + break + else: + content.add(sc) + if not closed: + self.recordError(esError, "Unclosed SQL string literal", ErrUnclosedString, strStart) + self.emitToken(tkString, content, strStart) + elif c == '"': + let strStart = self.currentPos() + discard self.advance() + let content = "\"" & self.parseQuotedString('"', strStart) + self.emitToken(tkString, content, strStart) + elif isDigit(c) or (c == '.' and self.hasMore() and self.peek(1).isDigit()): + let numStart = self.currentPos() + var num = "" + while self.hasMore() and (self.peek().isDigit() or self.peek() in {'.', 'e', 'E', '+', '-'}): + num.add(self.advance()) + self.emitToken(tkNumber, num, numStart) + elif isAlpha(c) or c == '_': + let idStart = self.currentPos() + var ident = "" + while self.hasMore() and (isAlphaNum(self.peek()) or self.peek() in {'_', '$'}): + ident.add(self.advance()) + self.emitToken(tkIdentifier, ident, idStart) + elif c == '(': + self.emitBracketToken(kOpenParen, $self.advance(), startPos) + elif c == ')': + self.closeBracket(')', kCloseParen, startPos) + elif c == ';': + self.emitToken(tkPunctuation, $self.advance(), startPos) + elif c in {'+', '-', '*', '/', '%', '=', '<', '>', '!', '|', '&', ','}: + self.emitToken(tkOperator, $self.advance(), startPos) + else: + self.emitToken(tkSpecial, $self.advance(), startPos) + + self.finishTokenizeStep(posBefore) + + self.emitToken(tkEndOfFile, "", self.currentPos()) + when defined(nimcheckDebug): + if self.options.debugMode: debugLeave("SQL_TOKENIZER", &"Complete: {self.tokens.len} tokens") + +# --------------------------------------------------------------------------- +# Markdown tokenizer +# --------------------------------------------------------------------------- + +type + MarkdownTokenizer* = ref object of TokenizerBase + fenceStack*: seq[string] + +proc newMarkdownTokenizer*(source: string, options: ValidationOptions = newValidationOptions()): MarkdownTokenizer = + MarkdownTokenizer( + source: source, sourceLen: source.len, options: options, + tokens: @[], pos: 0, line: 1, column: 1, bracketStack: @[], errors: @[], eof: false, + fenceStack: @[] + ) + +method tokenize*(self: MarkdownTokenizer) = + when defined(nimcheckDebug): + if self.options.debugMode: debugEnter("MD_TOKENIZER", "Starting Markdown tokenization") + + var lineNum = 1 + for line in self.source.splitLines(keepEol = true): + let lineStart = self.currentPos() + let stripped = line.strip() + + if stripped.startsWith("```"): + let fence = stripped + if self.fenceStack.len > 0 and self.fenceStack[^1] == "```": + discard self.fenceStack.pop() + self.emitToken(tkKeyword, fence, lineStart) + else: + self.fenceStack.add("```") + self.emitToken(tkKeyword, fence, lineStart) + elif stripped.startsWith("~~~"): + let fence = stripped + if self.fenceStack.len > 0 and self.fenceStack[^1] == "~~~": + discard self.fenceStack.pop() + self.emitToken(tkKeyword, fence, lineStart) + else: + self.fenceStack.add("~~~") + self.emitToken(tkKeyword, fence, lineStart) + elif stripped.len > 0 and stripped[0] == '#': + self.emitToken(tkDirective, stripped, lineStart) + elif '[' in line and "](" in line: + var idx = 0 + while idx < line.len: + if line[idx] == '[': + let linkStart = idx + let closeBracket = line.find(']', idx + 1) + if closeBracket >= 0 and closeBracket + 1 < line.len and line[closeBracket + 1] == '(': + let closeParen = line.find(')', closeBracket + 2) + if closeParen < 0: + self.recordError(esError, &"Unclosed Markdown link on line {lineNum}", + ErrUnclosedBlock, newSourcePosition(lineNum, linkStart + 1, 0), + hint = "Add a closing ')' to complete the link") + idx = if closeParen >= 0: closeParen + 1 else: line.len + else: + idx += 1 + else: + idx += 1 + self.emitToken(tkTemplateTag, line, lineStart) + else: + var backtickCount = 0 + for c in line: + if c == '`': backtickCount += 1 + if backtickCount mod 2 != 0: + self.recordError(esWarning, &"Unbalanced inline code backticks on line {lineNum}", + WarnInconsistentNaming, newSourcePosition(lineNum, 1, 0)) + self.emitToken(tkTemplateTag, line, lineStart) + + self.pos += line.len + self.line += 1 + self.column = 1 + lineNum += 1 + + if self.fenceStack.len > 0: + self.recordError(esError, "Unclosed fenced code block", + ErrUnclosedBlock, newSourcePosition(lineNum, 1, 0), + hint = "Add a closing ``` or ~~~ fence") + + self.emitToken(tkEndOfFile, "", self.currentPos()) + when defined(nimcheckDebug): + if self.options.debugMode: debugLeave("MD_TOKENIZER", &"Complete: {self.tokens.len} tokens") + +# --------------------------------------------------------------------------- +# Dockerfile tokenizer +# --------------------------------------------------------------------------- + +type + DockerfileTokenizer* = ref object of TokenizerBase + +const + dockerInstructions* = @[ + "FROM", "RUN", "CMD", "LABEL", "EXPOSE", "ENV", "ADD", "COPY", + "ENTRYPOINT", "VOLUME", "USER", "WORKDIR", "ARG", "ONBUILD", + "STOPSIGNAL", "HEALTHCHECK", "SHELL" + ].toHashSet() + +proc newDockerfileTokenizer*(source: string, options: ValidationOptions = newValidationOptions()): DockerfileTokenizer = + DockerfileTokenizer( + source: source, sourceLen: source.len, options: options, + tokens: @[], pos: 0, line: 1, column: 1, bracketStack: @[], errors: @[], eof: false + ) + +method tokenize*(self: DockerfileTokenizer) = + when defined(nimcheckDebug): + if self.options.debugMode: debugEnter("DOCKER_TOKENIZER", "Starting Dockerfile tokenization") + + var lineNum = 1 + var lines = self.source.splitLines() + var i = 0 + while i < lines.len: + var line = lines[i] + let lineStart = newSourcePosition(lineNum, 1, 0) + + while line.len > 0 and line[^1] == '\\' and i + 1 < lines.len: + line = line[0..^2] & " " & lines[i + 1].strip() + i += 1 + lineNum += 1 + + let stripped = line.strip() + if stripped.len == 0 or stripped[0] == '#': + if stripped.len > 0: + self.emitToken(tkComment, stripped, lineStart) + i += 1 + lineNum += 1 + continue + + let spaceIdx = stripped.find(' ') + let instruction = if spaceIdx >= 0: stripped[0..= 0: stripped[spaceIdx + 1 .. ^1] else: "" + if args.len > 0: + self.emitToken(tkString, args, lineStart) + for ch in args: + if ch == '[': + self.bracketStack.add(('[', lineStart)) + elif ch == ']': + if self.bracketStack.len > 0 and self.bracketStack[^1][0] == '[': + discard self.bracketStack.pop() + else: + self.recordError(esError, &"Unexpected ']' in Dockerfile instruction '{instruction}'", + ErrMismatchedBracket, lineStart) + elif ch == '"': + discard + let openQuotes = args.count('"') - args.count("\\\"") + if openQuotes mod 2 != 0: + self.recordError(esError, &"Unclosed double-quoted string in {instruction}", + ErrUnclosedString, lineStart, hint = "Add a closing '\"'") + else: + self.recordError(esWarning, &"Unknown Dockerfile instruction: {instruction}", + WarnInconsistentNaming, lineStart) + + i += 1 + lineNum += 1 + + for (_, pos) in self.bracketStack: + self.recordError(esError, "Unclosed '[' in Dockerfile JSON array", + ErrUnclosedBracket, pos, hint = "Add a closing ']'") + + self.pos = self.sourceLen + self.line = lineNum + self.emitToken(tkEndOfFile, "", self.currentPos()) + when defined(nimcheckDebug): + if self.options.debugMode: debugLeave("DOCKER_TOKENIZER", &"Complete: {self.tokens.len} tokens") + +# --------------------------------------------------------------------------- +# Makefile tokenizer +# --------------------------------------------------------------------------- + +type + MakefileTokenizer* = ref object of TokenizerBase + +proc newMakefileTokenizer*(source: string, options: ValidationOptions = newValidationOptions()): MakefileTokenizer = + MakefileTokenizer( + source: source, sourceLen: source.len, options: options, + tokens: @[], pos: 0, line: 1, column: 1, bracketStack: @[], errors: @[], eof: false + ) + +method tokenize*(self: MakefileTokenizer) = + when defined(nimcheckDebug): + if self.options.debugMode: debugEnter("MAKEFILE_TOKENIZER", "Starting Makefile tokenization") + + var lineNum = 1 + for line in self.source.splitLines(): + let lineStart = newSourcePosition(lineNum, 1, 0) + + if line.len == 0: + self.emitToken(tkNewline, "", lineStart) + elif line[0] == '#': + self.emitToken(tkComment, line, lineStart) + elif line[0] == '\t' or (line.len >= 2 and line[0] == '\\' and line[1] == 't'): + let recipe = if line[0] == '\t': line else: '\t' & line[2..^1] + self.emitToken(tkSpecial, recipe, lineStart) + var parenDepth = 0 + var inQuote = false + for i, c in recipe: + if c == '"' and (i == 0 or recipe[i - 1] != '\\'): + inQuote = not inQuote + elif c == '(' and not inQuote: + parenDepth += 1 + self.bracketStack.add(('(', newSourcePosition(lineNum, i + 1, 0))) + elif c == ')' and not inQuote: + if parenDepth > 0 and self.bracketStack.len > 0: + parenDepth -= 1 + discard self.bracketStack.pop() + else: + self.recordError(esError, &"Unexpected ')' in Makefile recipe on line {lineNum}", + ErrMismatchedBracket, newSourcePosition(lineNum, i + 1, 0)) + if inQuote: + self.recordError(esError, &"Unclosed double-quoted string in Makefile recipe on line {lineNum}", + ErrUnclosedString, lineStart, hint = "Add a closing '\"'") + elif ':' in line and not line.strip().startsWith("#"): + let colonIdx = line.find(':') + let target = line[0.. 0: + self.emitToken(tkString, deps, lineStart) + elif line.strip().len > 0 and line[0] notin {'\t', '#'}: + if line.contains('='): + self.emitToken(tkAssignment, line.strip(), lineStart) + elif line.strip().startsWith(".PHONY") or line.strip().startsWith(".SUFFIXES"): + self.emitToken(tkDirective, line.strip(), lineStart) + else: + self.recordError(esWarning, &"Makefile line {lineNum} should start with tab for recipes", + "W1001", lineStart, hint = "Recipe commands must be indented with a tab character") + + lineNum += 1 + + for (_, pos) in self.bracketStack: + self.recordError(esError, "Unclosed '(' in Makefile variable expansion", + ErrUnclosedBracket, pos, hint = "Add a closing ')'") + + self.pos = self.sourceLen + self.line = lineNum + self.emitToken(tkEndOfFile, "", self.currentPos()) + when defined(nimcheckDebug): + if self.options.debugMode: debugLeave("MAKEFILE_TOKENIZER", &"Complete: {self.tokens.len} tokens") \ No newline at end of file diff --git a/src/nimcheck/tokenizers/javascript_tokenizer.nim b/src/nimcheck/tokenizers/javascript_tokenizer.nim index c2a8b1d..7dfdbb4 100644 --- a/src/nimcheck/tokenizers/javascript_tokenizer.nim +++ b/src/nimcheck/tokenizers/javascript_tokenizer.nim @@ -8,7 +8,7 @@ type JavaScriptTokenizer* = ref object of TokenizerBase const - jsKeywords = @[ + jsKeywords* = @[ "async", "await", "break", "case", "catch", "class", "const", "continue", "debugger", "default", "delete", "do", "else", "enum", "export", "extends", "false", "finally", "for", "function", "if", "import", "in", "instanceof", @@ -19,7 +19,7 @@ const "as", "any", "boolean", "number", "string", "undefined", "never", "keyof", "infer", "unknown" ] - jsKeywordSet = jsKeywords.toHashSet() + jsKeywordSet* = jsKeywords.toHashSet() proc newJavaScriptTokenizer*(source: string, options: ValidationOptions = newValidationOptions()): JavaScriptTokenizer = result = JavaScriptTokenizer( @@ -178,19 +178,13 @@ method tokenize*(self: JavaScriptTokenizer) = # Brackets elif c == '(': self.emitBracketToken(kOpenParen, $self.advance(), startPos) elif c == ')': - self.emitBracketToken(kCloseParen, $self.advance(), startPos) - if self.bracketStack.len > 0 and self.bracketStack[^1][0] == '(': - discard self.bracketStack.pop() + self.closeBracket(')', kCloseParen, startPos) elif c == '[': self.emitBracketToken(kOpenBracket, $self.advance(), startPos) elif c == ']': - self.emitBracketToken(kCloseBracket, $self.advance(), startPos) - if self.bracketStack.len > 0 and self.bracketStack[^1][0] == '[': - discard self.bracketStack.pop() + self.closeBracket(']', kCloseBracket, startPos) elif c == '{': self.emitBracketToken(kOpenBrace, $self.advance(), startPos) elif c == '}': - self.emitBracketToken(kCloseBrace, $self.advance(), startPos) - if self.bracketStack.len > 0 and self.bracketStack[^1][0] == '{': - discard self.bracketStack.pop() + self.closeBracket('}', kCloseBrace, startPos) # Operators elif c in {'+', '-', '*', '/', '%', '^', '~', '|', '&', '<', '>', '=', '!', '?'}: diff --git a/src/nimcheck/tokenizers/jinja_tokenizer.nim b/src/nimcheck/tokenizers/jinja_tokenizer.nim index 5f20874..397f000 100644 --- a/src/nimcheck/tokenizers/jinja_tokenizer.nim +++ b/src/nimcheck/tokenizers/jinja_tokenizer.nim @@ -42,6 +42,7 @@ method tokenize*(self: JinjaTokenizer) = debugEnter("JINJA_TOKENIZER", "Starting Jinja tokenization") while self.hasMore(): + let posBefore = self.pos let startPos = self.currentPos() let c = self.peek() @@ -116,6 +117,11 @@ method tokenize*(self: JinjaTokenizer) = text.add(self.advance()) if text.len > 0: self.emitToken(tkTemplateTag, text, startPos) + else: + # Lone '{' or other delimiter — must always advance (prevents infinite loop) + self.emitToken(tkSpecial, $self.advance(), startPos) + + self.finishTokenizeStep(posBefore) self.emitToken(tkEndOfFile, "", self.currentPos()) when defined(nimcheckDebug): diff --git a/src/nimcheck/tokenizers/lang_tokenizers.nim b/src/nimcheck/tokenizers/lang_tokenizers.nim new file mode 100644 index 0000000..9fc1da7 --- /dev/null +++ b/src/nimcheck/tokenizers/lang_tokenizers.nim @@ -0,0 +1,447 @@ +## Language tokenizers: Kotlin, Lua, Swift. +## +## Standalone tokenizers for languages that do not fit the +## C-like or template-based tokenization patterns. + +import std/[strformat, sets] +{.warning[UnusedImport]:off.} +import ../core/types, ../core/tokenizerbase +import extended_tokenizers + +# --------------------------------------------------------------------------- +# Kotlin tokenizer +# --------------------------------------------------------------------------- + +type + KotlinTokenizer* = ref object of TokenizerBase + +const + kotlinKeywords* = @[ + "abstract", "actual", "annotation", "as", "as?", "break", "by", "catch", + "class", "companion", "const", "constructor", "continue", "crossinline", + "data", "delegate", "do", "dynamic", "else", "enum", "expect", "external", + "false", "field", "file", "final", "finally", "for", "fun", "if", "import", + "in", "infix", "init", "inline", "inner", "interface", "internal", "is", + "it", "lateinit", "noinline", "null", "object", "open", "operator", "out", + "override", "package", "param", "private", "property", "protected", "public", + "receiver", "reified", "return", "sealed", "set", "setparam", "super", + "suspend", "tailrec", "this", "throw", "true", "try", "typealias", + "typeof", "val", "var", "vararg", "when", "where", "while" + ] + kotlinKeywordSet* = kotlinKeywords.toHashSet() + +proc newKotlinTokenizer*(source: string, options: ValidationOptions = newValidationOptions()): KotlinTokenizer = + KotlinTokenizer( + source: source, sourceLen: source.len, options: options, + tokens: @[], pos: 0, line: 1, column: 1, bracketStack: @[], errors: @[], eof: false + ) + +method tokenize*(self: KotlinTokenizer) = + when defined(nimcheckDebug): + if self.options.debugMode: debugEnter("KOTLIN_TOKENIZER", "Starting Kotlin tokenization") + + while self.hasMore(): + let posBefore = self.pos + let startPos = self.currentPos() + let c = self.peek() + + if c in {' ', '\t', '\r'}: + let wsStart = self.currentPos() + var ws = "" + while self.hasMore() and self.peek() in {' ', '\t', '\r'}: + ws.add(self.advance()) + self.emitToken(tkWhitespace, ws, wsStart) + + elif c == '\n': + self.emitToken(tkNewline, $self.advance(), startPos) + + elif self.peekString(2) == "//": + let comStart = self.currentPos() + discard self.advance() + discard self.advance() + self.emitToken(tkComment, "//" & self.parseLineComment(), comStart) + + elif self.peekString(2) == "/*": + let comStart = self.currentPos() + discard self.advance() + discard self.advance() + let content = self.parseBlockComment("/*", "*/") + let isDoc = content.startsWith("*") + self.emitToken(if isDoc: tkDocComment else: tkComment, "/*" & content & "*/", comStart) + + elif c == '"': + let strStart = self.currentPos() + discard self.advance() + let content = '"' & self.parseQuotedString('"', strStart) + self.emitToken(tkString, content, strStart) + + elif c == '\'': + let strStart = self.currentPos() + discard self.advance() + let content = "'" & self.parseQuotedString('\'', strStart) + self.emitToken(tkString, content, strStart) + + elif c == '`': + # Kotlin backtick identifier + let btStart = self.currentPos() + discard self.advance() + var ident = "`" + while self.hasMore() and self.peek() != '`': + ident.add(self.advance()) + if self.hasMore(): + ident.add(self.advance()) + self.emitToken(tkIdentifier, ident, btStart) + + elif isDigit(c) or (c == '.' and self.hasMore() and self.peek(1).isDigit()): + let numStart = self.currentPos() + var num = "" + while self.hasMore() and (self.peek().isDigit() or self.peek() in {'.', 'x', 'X', 'e', 'E', '+', '-', '_', 'L', 'l', 'F', 'f'}): + num.add(self.advance()) + self.emitToken(tkNumber, num, numStart) + + elif isAlpha(c) or c == '_': + let idStart = self.currentPos() + var ident = "" + while self.hasMore() and (isAlphaNum(self.peek()) or self.peek() == '_'): + ident.add(self.advance()) + if kotlinKeywordSet.contains(ident): + self.emitToken(tkKeyword, ident, idStart) + else: + self.emitToken(tkIdentifier, ident, idStart) + + elif c == '(': + self.emitBracketToken(kOpenParen, $self.advance(), startPos) + elif c == ')': + self.closeBracket(')', kCloseParen, startPos) + elif c == '[': + self.emitBracketToken(kOpenBracket, $self.advance(), startPos) + elif c == ']': + self.closeBracket(']', kCloseBracket, startPos) + elif c == '{': + self.emitBracketToken(kOpenBrace, $self.advance(), startPos) + elif c == '}': + self.closeBracket('}', kCloseBrace, startPos) + + elif c in {'+', '-', '*', '/', '%', '&', '|', '^', '~', '!', '?', ':', '=', '<', '>', '@'}: + let opStart = self.currentPos() + var op = "" + while self.hasMore() and self.peek() in {'+', '-', '*', '/', '%', '&', '|', '^', '~', '!', '?', ':', '=', '<', '>', '@'}: + op.add(self.advance()) + if op in ["=", "+=", "-=", "*=", "/=", "%="]: + self.emitToken(tkAssignment, op, opStart) + else: + self.emitToken(tkOperator, op, opStart) + + elif c in {'.', ',', ';'}: + self.emitToken(tkPunctuation, $self.advance(), startPos) + + elif c == '#': + self.emitToken(tkDirective, $self.advance(), startPos) + + else: + self.emitToken(tkSpecial, $self.advance(), startPos) + + self.finishTokenizeStep(posBefore) + + self.emitToken(tkEndOfFile, "", self.currentPos()) + when defined(nimcheckDebug): + if self.options.debugMode: debugLeave("KOTLIN_TOKENIZER", &"Complete: {self.tokens.len} tokens") + +# --------------------------------------------------------------------------- +# Lua tokenizer +# --------------------------------------------------------------------------- + +type + LuaTokenizer* = ref object of TokenizerBase + +const + luaKeywords* = @[ + "and", "break", "do", "else", "elseif", "end", "false", "for", "function", + "goto", "if", "in", "local", "nil", "not", "or", "repeat", "return", + "then", "true", "until", "while" + ] + luaKeywordSet* = luaKeywords.toHashSet() + +proc newLuaTokenizer*(source: string, options: ValidationOptions = newValidationOptions()): LuaTokenizer = + LuaTokenizer( + source: source, sourceLen: source.len, options: options, + tokens: @[], pos: 0, line: 1, column: 1, bracketStack: @[], errors: @[], eof: false + ) + +method tokenize*(self: LuaTokenizer) = + when defined(nimcheckDebug): + if self.options.debugMode: debugEnter("LUA_TOKENIZER", "Starting Lua tokenization") + + while self.hasMore(): + let posBefore = self.pos + let startPos = self.currentPos() + let c = self.peek() + + if c in {' ', '\t', '\r'}: + let wsStart = self.currentPos() + var ws = "" + while self.hasMore() and self.peek() in {' ', '\t', '\r'}: + ws.add(self.advance()) + self.emitToken(tkWhitespace, ws, wsStart) + + elif c == '\n': + self.emitToken(tkNewline, $self.advance(), startPos) + + elif self.peekString(2) == "--": + let comStart = self.currentPos() + discard self.advance() + discard self.advance() + if self.peekString(2) == "[[": + # Long block comment --[[ ... ]] + discard self.advance() + discard self.advance() + let content = self.parseBlockComment("[[", "]]") + self.emitToken(tkComment, "--[[" & content & "]]", comStart) + else: + self.emitToken(tkComment, "--" & self.parseLineComment(), comStart) + + elif c == '"' or c == '\'': + let strStart = self.currentPos() + discard self.advance() + let content = $c & self.parseQuotedString(c, strStart) + self.emitToken(tkString, content, strStart) + + elif c == '[' and self.peekString(2) == "[[": + # Long string literal [[ ... ]] + let lsStart = self.currentPos() + discard self.advance() + discard self.advance() + var content = "[[" + var foundClosing = false + while self.hasMore(): + if self.peekString(2) == "]]": + content.add("]]") + discard self.advance() + discard self.advance() + foundClosing = true + break + content.add(self.advance()) + if not foundClosing: + self.recordError(esError, "Unclosed long string literal", ErrUnclosedString, lsStart, + hint = "Add closing ']]'") + self.emitToken(tkString, content, lsStart) + + elif isDigit(c) or (c == '.' and self.hasMore() and self.peek(1).isDigit()): + let numStart = self.currentPos() + var num = "" + while self.hasMore() and (self.peek().isDigit() or self.peek() in {'.', 'e', 'E', '+', '-', 'x', 'X'}): + num.add(self.advance()) + self.emitToken(tkNumber, num, numStart) + + elif isAlpha(c) or c == '_': + let idStart = self.currentPos() + var ident = "" + while self.hasMore() and (isAlphaNum(self.peek()) or self.peek() == '_'): + ident.add(self.advance()) + if luaKeywordSet.contains(ident): + self.emitToken(tkKeyword, ident, idStart) + else: + self.emitToken(tkIdentifier, ident, idStart) + + elif c == '(': + self.emitBracketToken(kOpenParen, $self.advance(), startPos) + elif c == ')': + self.closeBracket(')', kCloseParen, startPos) + elif c == '[': + self.emitBracketToken(kOpenBracket, $self.advance(), startPos) + elif c == ']': + self.closeBracket(']', kCloseBracket, startPos) + elif c == '{': + self.emitBracketToken(kOpenBrace, $self.advance(), startPos) + elif c == '}': + self.closeBracket('}', kCloseBrace, startPos) + + elif c in {'+', '-', '*', '/', '%', '^', '#', '=', '<', '>', '~', ':', '&', '|', '!'}: + let opStart = self.currentPos() + var op = "" + while self.hasMore() and self.peek() in {'+', '-', '*', '/', '%', '^', '#', '=', '<', '>', '~', ':', '&', '|', '!'}: + op.add(self.advance()) + if op == "=": + self.emitToken(tkAssignment, op, opStart) + else: + self.emitToken(tkOperator, op, opStart) + + elif c in {'.', ',', ';'}: + self.emitToken(tkPunctuation, $self.advance(), startPos) + + elif c == '@': + self.emitToken(tkSpecial, $self.advance(), startPos) + + else: + self.emitToken(tkSpecial, $self.advance(), startPos) + + self.finishTokenizeStep(posBefore) + + self.emitToken(tkEndOfFile, "", self.currentPos()) + when defined(nimcheckDebug): + if self.options.debugMode: debugLeave("LUA_TOKENIZER", &"Complete: {self.tokens.len} tokens") + +# --------------------------------------------------------------------------- +# Swift tokenizer +# --------------------------------------------------------------------------- + +type + SwiftTokenizer* = ref object of TokenizerBase + +const + swiftKeywords* = @[ + "Any", "as", "associatedtype", "async", "await", "break", "case", "catch", + "class", "continue", "default", "defer", "deinit", "do", "else", "enum", + "extension", "fallthrough", "false", "fileprivate", "for", "func", "guard", + "if", "import", "in", "indirect", "infix", "init", "inout", "internal", + "is", "lazy", "let", "nil", "nonisolated", "open", "operator", "optional", + "override", "package", "postfix", "precedencegroup", "prefix", "private", + "protocol", "public", "rethrows", "return", "self", "Self", "static", + "struct", "subscript", "super", "switch", "throw", "throws", "true", + "try", "Type", "typealias", "unowned", "var", "weak", "where", "while", + "some", "any", "macro", "actor", "isolated", "distributed", "consuming", + "borrowing", "nonconsuming", "_borrowing" + ] + swiftKeywordSet* = swiftKeywords.toHashSet() + +proc newSwiftTokenizer*(source: string, options: ValidationOptions = newValidationOptions()): SwiftTokenizer = + SwiftTokenizer( + source: source, sourceLen: source.len, options: options, + tokens: @[], pos: 0, line: 1, column: 1, bracketStack: @[], errors: @[], eof: false + ) + +method tokenize*(self: SwiftTokenizer) = + when defined(nimcheckDebug): + if self.options.debugMode: debugEnter("SWIFT_TOKENIZER", "Starting Swift tokenization") + + while self.hasMore(): + let posBefore = self.pos + let startPos = self.currentPos() + let c = self.peek() + + if c in {' ', '\t', '\r'}: + let wsStart = self.currentPos() + var ws = "" + while self.hasMore() and self.peek() in {' ', '\t', '\r'}: + ws.add(self.advance()) + self.emitToken(tkWhitespace, ws, wsStart) + + elif c == '\n': + self.emitToken(tkNewline, $self.advance(), startPos) + + elif self.peekString(2) == "//": + let comStart = self.currentPos() + discard self.advance() + discard self.advance() + self.emitToken(tkComment, "//" & self.parseLineComment(), comStart) + + elif self.peekString(2) == "/*": + let comStart = self.currentPos() + discard self.advance() + discard self.advance() + if self.hasMore() and self.peek() == '*': + # Doc comment /** ... */ + discard self.advance() + let content = self.parseBlockComment("*", "*/") + self.emitToken(tkDocComment, "/**" & content & "*/", comStart) + else: + let content = self.parseBlockComment("/*", "*/") + self.emitToken(tkComment, "/*" & content & "*/", comStart) + + elif c == '"': + let strStart = self.currentPos() + discard self.advance() + # Check for multiline string literal """""" + if self.peekString(2) == "\"\"\"...": + discard + var content = "\"" + var foundClosing = false + while self.hasMore(): + let sc = self.advance() + if sc == '\\': + content.add(sc) + if self.hasMore(): content.add(self.advance()) + elif sc == '"': + content.add('"') + foundClosing = true + break + elif sc == '\n': + self.recordError(esError, "Newline in Swift string literal", ErrUnclosedString, strStart, + hint = "Use triple-quoted string \"\"\" for multiline strings") + break + else: + content.add(sc) + if not foundClosing: + self.recordError(esError, "Unclosed Swift string literal", ErrUnclosedString, strStart) + self.emitToken(tkString, content, strStart) + + elif c == '`': + # Swift backtick-escaped identifier + let btStart = self.currentPos() + discard self.advance() + var ident = "`" + while self.hasMore() and self.peek() != '`': + ident.add(self.advance()) + if self.hasMore(): + ident.add(self.advance()) + self.emitToken(tkIdentifier, ident, btStart) + + elif isDigit(c) or (c == '.' and self.hasMore() and self.peek(1).isDigit()): + let numStart = self.currentPos() + var num = "" + while self.hasMore() and (self.peek().isDigit() or self.peek() in {'.', 'x', 'X', 'o', 'O', 'b', 'B', 'e', 'E', '+', '-', '_'}): + num.add(self.advance()) + self.emitToken(tkNumber, num, numStart) + + elif isAlpha(c) or c == '_': + let idStart = self.currentPos() + var ident = "" + while self.hasMore() and (isAlphaNum(self.peek()) or self.peek() in {'_', '$'}): + ident.add(self.advance()) + if swiftKeywordSet.contains(ident): + self.emitToken(tkKeyword, ident, idStart) + else: + self.emitToken(tkIdentifier, ident, idStart) + + elif c == '(': + self.emitBracketToken(kOpenParen, $self.advance(), startPos) + elif c == ')': + self.closeBracket(')', kCloseParen, startPos) + elif c == '[': + self.emitBracketToken(kOpenBracket, $self.advance(), startPos) + elif c == ']': + self.closeBracket(']', kCloseBracket, startPos) + elif c == '{': + self.emitBracketToken(kOpenBrace, $self.advance(), startPos) + elif c == '}': + self.closeBracket('}', kCloseBrace, startPos) + + elif c in {'+', '-', '*', '/', '%', '&', '|', '^', '~', '!', '?', ':', '=', '<', '>', '.'}: + let opStart = self.currentPos() + var op = "" + while self.hasMore() and self.peek() in {'+', '-', '*', '/', '%', '&', '|', '^', '~', '!', '?', ':', '=', '<', '>', '.'}: + op.add(self.advance()) + if op == "=": + self.emitToken(tkAssignment, op, opStart) + else: + self.emitToken(tkOperator, op, opStart) + + elif c in {',', ';', ':'}: + self.emitToken(tkPunctuation, $self.advance(), startPos) + + elif c == '#': + let hashStart = self.currentPos() + var directive = "#" + discard self.advance() + while self.hasMore() and (isAlphaNum(self.peek()) or self.peek() == '_'): + directive.add(self.advance()) + self.emitToken(tkDirective, directive, hashStart) + + else: + self.emitToken(tkSpecial, $self.advance(), startPos) + + self.finishTokenizeStep(posBefore) + + self.emitToken(tkEndOfFile, "", self.currentPos()) + when defined(nimcheckDebug): + if self.options.debugMode: debugLeave("SWIFT_TOKENIZER", &"Complete: {self.tokens.len} tokens") diff --git a/src/nimcheck/tokenizers/nim_tokenizer.nim b/src/nimcheck/tokenizers/nim_tokenizer.nim index 4334b7b..0800780 100644 --- a/src/nimcheck/tokenizers/nim_tokenizer.nim +++ b/src/nimcheck/tokenizers/nim_tokenizer.nim @@ -184,17 +184,38 @@ method tokenize*(self: NimTokenizer) = let chStart = self.currentPos() discard self.advance() # ' var content = "'" - if self.hasMore(): - if self.peek() == '\\': - content.add(self.advance()) - if self.hasMore(): content.add(self.advance()) - else: - content.add(self.advance()) - if self.hasMore() and self.peek() == '\'': - content.add(self.advance()) + var charCount = 0 + while self.hasMore(): + let cc = self.peek() + if cc == '\'': + discard self.advance() + content.add('\'') + if charCount == 0: + self.recordError(esError, "Empty character literal", ErrUnclosedString, chStart, + hint = "Character literal must contain exactly one character") + elif charCount > 1: + self.recordError(esError, + &"Character literal contains {charCount} characters instead of 1", + ErrInvalidSyntax, chStart, + hint = "Use a string for multi-character sequences") self.emitToken(tkString, content, chStart) - else: + break + elif cc == '\n': self.recordError(esError, "Unclosed character literal", ErrUnclosedString, chStart) + self.emitToken(tkString, content, chStart) + break + elif cc == '\\': + content.add(self.advance()) + if self.hasMore(): + content.add(self.advance()) + charCount += 1 + else: + content.add(self.advance()) + charCount += 1 + + if not self.hasMore() and charCount > 0: + self.recordError(esError, "Unclosed character literal", ErrUnclosedString, chStart) + self.emitToken(tkString, content, chStart) # Numbers elif isDigit(c): diff --git a/src/nimcheck/tokenizers/php_tokenizer.nim b/src/nimcheck/tokenizers/php_tokenizer.nim index b30e10f..afbf8ee 100644 --- a/src/nimcheck/tokenizers/php_tokenizer.nim +++ b/src/nimcheck/tokenizers/php_tokenizer.nim @@ -137,9 +137,9 @@ method tokenize*(self: PHPTokenizer) = foundClosing = true break elif sc == '\n': + self.recordError(esError, "Unclosed string at end of line", ErrUnclosedString, strStart) self.emitToken(tkString, content, strStart) foundClosing = true - foundClosing = true break else: content.add(sc) diff --git a/src/nimcheck/tokenizers/python_tokenizer.nim b/src/nimcheck/tokenizers/python_tokenizer.nim index 9345794..33c56df 100644 --- a/src/nimcheck/tokenizers/python_tokenizer.nim +++ b/src/nimcheck/tokenizers/python_tokenizer.nim @@ -341,7 +341,8 @@ method tokenize*(self: PythonTokenizer) = let uStart = self.currentPos() self.emitToken(tkError, $self.advance(), uStart) - # Add dedent tokens for remaining indentation at EOF + # Flush remaining indentation at EOF + # Trailing indent at EOF is valid Python - emit dedent tokens silently. while self.indentStack.len > 1: discard self.indentStack.pop() self.emitToken(tkSpecial, "", self.currentPos()) diff --git a/src/nimcheck/tokenizers/type_xml_tokenizer.nim b/src/nimcheck/tokenizers/type_xml_tokenizer.nim new file mode 100644 index 0000000..4cd435e --- /dev/null +++ b/src/nimcheck/tokenizers/type_xml_tokenizer.nim @@ -0,0 +1,452 @@ +## Improved TypeScript and XML tokenizers. +## +## TypeScript builds on JavaScriptTokenizer with TS-specific features. +## XML provides a full XML/SGML-style tag tokenizer. + +import std/[strformat, sets] +{.warning[UnusedImport]:off.} +import ../core/types, ../core/tokenizerbase +import javascript_tokenizer + +# --------------------------------------------------------------------------- +# Improved TypeScript tokenizer (extends JavaScript tokenizer) +# --------------------------------------------------------------------------- + +type + TypeScriptTokenizer* = ref object of JavaScriptTokenizer + +const + tsExtraKeywords* = @[ + "interface", "type", "declare", "module", "namespace", "abstract", + "readonly", "as", "any", "boolean", "number", "string", "undefined", + "never", "keyof", "infer", "unknown", "satisfies", "asserts", + "is", "enum", "const", "override", "private", "protected", "public", + "static", "implements", "extends", "infer", "template", "using" + ] + tsExtraKeywordSet* = tsExtraKeywords.toHashSet() + +proc newTypeScriptTokenizer*(source: string, options: ValidationOptions = newValidationOptions()): TypeScriptTokenizer = + TypeScriptTokenizer( + source: source, sourceLen: source.len, options: options, + tokens: @[], pos: 0, line: 1, column: 1, bracketStack: @[], errors: @[], eof: false + ) + +method tokenize*(self: TypeScriptTokenizer) = + when defined(nimcheckDebug): + if self.options.debugMode: debugEnter("TS_TOKENIZER", "Starting TypeScript tokenization") + + while self.hasMore(): + let startPos = self.currentPos() + let c = self.peek() + + if c in {' ', '\t', '\r'}: + let wsStart = self.currentPos() + var ws = "" + while self.hasMore() and self.peek() in {' ', '\t', '\r'}: + ws.add(self.advance()) + self.emitToken(tkWhitespace, ws, wsStart) + + elif c == '\n': + self.emitToken(tkNewline, $self.advance(), startPos) + + # Template literals + elif c == '`': + let tlStart = self.currentPos() + discard self.advance() + var content = "`" + var braceDepth = 0 + var foundClosing = false + while self.hasMore(): + let tc = self.advance() + if tc == '\\': + content.add(tc) + if self.hasMore(): content.add(self.advance()) + elif tc == '`' and braceDepth == 0: + content.add('`') + self.emitToken(tkString, content, tlStart) + foundClosing = true + break + elif tc == '$' and self.hasMore() and self.peek() == '{' and braceDepth == 0: + content.add("${") + discard self.advance() + braceDepth += 1 + elif tc == '{': braceDepth += 1; content.add(tc) + elif tc == '}': braceDepth -= 1; content.add(tc) + else: content.add(tc) + if not foundClosing: + self.recordError(esError, "Unclosed template literal", ErrUnclosedString, tlStart) + self.emitToken(tkString, content, tlStart) + + elif self.peekString(2) == "//": + let comStart = self.currentPos() + discard self.advance() + discard self.advance() + self.emitToken(tkComment, "//" & self.parseLineComment(), comStart) + + elif self.peekString(2) == "/*": + let comStart = self.currentPos() + discard self.advance() + discard self.advance() + let content = self.parseBlockComment("/*", "*/") + let isDoc = content.startsWith("*") + self.emitToken(if isDoc: tkDocComment else: tkComment, "/*" & content & "*/", comStart) + + elif c == '/' and self.pos > 0: + let prevChar = if self.pos > 0: self.source[self.pos - 1] else: '\0' + if prevChar in {' ', '\t', '\n', '\r', '=', '(', '[', '{', '!', '&', '|', ':', ';', ',', '^', '~', '?'}: + let regStart = self.currentPos() + discard self.advance() + var regex = "/" + var foundClosing = false + while self.hasMore(): + let rc = self.advance() + if rc == '\\': + regex.add(rc) + if self.hasMore(): regex.add(self.advance()) + elif rc == '/': + regex.add('/') + while self.hasMore() and (isAlpha(self.peek()) or self.peek() in {'$', '_'}): + regex.add(self.advance()) + self.emitToken(tkSpecial, regex, regStart) + foundClosing = true + break + else: + regex.add(rc) + if not foundClosing: + self.emitToken(tkOperator, "/", regStart) + else: + self.emitToken(tkOperator, "/", startPos) + discard self.advance() + + elif c == '"' or c == '\'': + let quote = c + let strStart = self.currentPos() + discard self.advance() + var content = $quote + var foundClosing = false + while self.hasMore(): + let sc = self.advance() + if sc == '\\': + content.add(sc) + if self.hasMore(): content.add(self.advance()) + elif sc == quote: + content.add(quote) + self.emitToken(tkString, content, strStart) + foundClosing = true + break + elif sc == '\n': + self.recordError(esError, "Newline in string", ErrUnclosedString, strStart) + break + else: + content.add(sc) + if not foundClosing: + self.recordError(esError, "Unclosed string", ErrUnclosedString, strStart) + self.emitToken(tkString, content, strStart) + + elif isDigit(c): + let numStart = self.currentPos() + var num = "" + while self.hasMore() and (isAlphaNum(self.peek()) or self.peek() in {'.', 'x', 'X', 'o', 'O', 'b', 'B', '_'}): + num.add(self.advance()) + self.emitToken(tkNumber, num, numStart) + + elif isAlpha(c) or c == '$' or c == '_': + let idStart = self.currentPos() + var ident = "" + while self.hasMore() and (isAlphaNum(self.peek()) or self.peek() in {'_', '$'}) or self.peek() == '.': + # Support dotted names in types + if self.peek() == '.' and ident.len > 0 and isAlpha(self.source[self.pos - 1]): + ident.add(self.advance()) + continue + ident.add(self.advance()) + if jsKeywordSet.contains(ident): + self.emitToken(tkKeyword, ident, idStart) + elif tsExtraKeywordSet.contains(ident): + self.emitToken(tkKeyword, ident, idStart) + else: + self.emitToken(tkIdentifier, ident, idStart) + + elif c == '(': self.emitBracketToken(kOpenParen, $self.advance(), startPos) + elif c == ')': + self.closeBracket(')', kCloseParen, startPos) + elif c == '[': self.emitBracketToken(kOpenBracket, $self.advance(), startPos) + elif c == ']': + self.closeBracket(']', kCloseBracket, startPos) + elif c == '{': self.emitBracketToken(kOpenBrace, $self.advance(), startPos) + elif c == '}': + self.closeBracket('}', kCloseBrace, startPos) + + elif c in {'+', '-', '*', '/', '%', '^', '~', '|', '&', '<', '>', '=', '!', '?'}: + let opStart = self.currentPos() + var op = "" + while self.hasMore() and self.peek() in {'+', '-', '*', '/', '%', '^', '~', '|', '&', '<', '>', '=', '!', '?', '.'}: + let nc = self.peek() + if nc == '.' and op.len > 0: break + op.add(self.advance()) + if op in ["=", "=>"]: + self.emitToken(tkAssignment, op, opStart) + else: + self.emitToken(tkOperator, op, opStart) + + elif c == '.': + self.emitToken(tkPunctuation, $self.advance(), startPos) + elif c in {',', ';', ':'}: + self.emitToken(tkPunctuation, $self.advance(), startPos) + + elif c == '@': + # TS decorator + let atStart = self.currentPos() + var decorator = "@" + discard self.advance() + while self.hasMore() and (isAlphaNum(self.peek()) or self.peek() in {'_', '.'}): + decorator.add(self.advance()) + self.emitToken(tkDirective, decorator, atStart) + + elif c == '#': + let hashStart = self.currentPos() + var dir = "#" + discard self.advance() + while self.hasMore() and (isAlphaNum(self.peek()) or self.peek() == '_'): + dir.add(self.advance()) + self.emitToken(tkDirective, dir, hashStart) + + elif c == '<' and self.pos + 1 < self.sourceLen and (isAlpha(self.peek(1)) or self.peek(1) == '/'): + # XML-like JSX tag or TypeScript type parameter - treat as operator/template + self.emitBracketToken(kAngleOpen, $self.advance(), startPos) + + else: + self.emitToken(tkError, $self.advance(), startPos) + + self.emitToken(tkEndOfFile, "", self.currentPos()) + when defined(nimcheckDebug): + if self.options.debugMode: debugLeave("TS_TOKENIZER", &"Complete: {self.tokens.len} tokens") + +# --------------------------------------------------------------------------- +# XML tokenizer +# --------------------------------------------------------------------------- + +type + XMLTokenizer* = ref object of TokenizerBase + tagStack*: seq[string] + +const + xmlKeywords* = @["xml", "xsl", "schema", "doctype", "element", "attribute", + "entity", "notation", "simpleType", "complexType", "sequence", "choice", + "include", "import", "redefine", "override", "annotation", "documentation", + "appinfo", "any", "group", "attributeGroup", "restriction", "extension", + "union", "list", "all", "key", "keyref", "unique", "selector", "field", + "pattern", "enumeration", "length", "minLength", "maxLength", "minInclusive", + "maxInclusive", "minExclusive", "maxExclusive", "totalDigits", "fractionDigits", + "whiteSpace" + ] + xmlKeywordSet* = xmlKeywords.toHashSet() + +proc newXMLTokenizer*(source: string, options: ValidationOptions = newValidationOptions()): XMLTokenizer = + XMLTokenizer( + source: source, sourceLen: source.len, options: options, + tokens: @[], pos: 0, line: 1, column: 1, bracketStack: @[], errors: @[], eof: false, + tagStack: @[] + ) + +method tokenize*(self: XMLTokenizer) = + when defined(nimcheckDebug): + if self.options.debugMode: debugEnter("XML_TOKENIZER", "Starting XML tokenization") + + while self.hasMore(): + let startPos = self.currentPos() + let c = self.peek() + + if c in {' ', '\t', '\r'}: + let wsStart = self.currentPos() + var ws = "" + while self.hasMore() and self.peek() in {' ', '\t', '\r'}: + ws.add(self.advance()) + self.emitToken(tkWhitespace, ws, wsStart) + + elif c == '\n': + self.emitToken(tkNewline, $self.advance(), startPos) + + elif self.peekString(4) == "": + discard self.advance() + discard self.advance() + discard self.advance() + foundClosing = true + break + content.add(self.advance()) + if not foundClosing: + self.recordError(esError, "Unclosed XML comment", ErrUnclosedComment, comStart, + hint = "Add closing '-->'") + self.emitToken(tkComment, "", comStart) + + elif self.peekString(2) == " + let piStart = self.currentPos() + discard self.advance() + discard self.advance() + var content = "": + content.add("?>") + discard self.advance() + discard self.advance() + break + content.add(self.advance()) + self.emitToken(tkDirective, content, piStart) + + elif c == '<': + let tagStart = self.currentPos() + discard self.advance() + var isClosing = false + var isSelfClosing = false + if self.hasMore() and self.peek() == '/': + isClosing = true + discard self.advance() + # Read tag name + var tagName = "" + while self.hasMore() and self.peek() != '>' and self.peek() != '/' and self.peek() notin {' ', '\t', '\n', '\r'}: + tagName.add(self.advance()) + if tagName.len > 0: + if isClosing: + self.emitToken(tkSpecial, " 0 and self.tagStack[^1] == tagName: + discard self.tagStack.pop() + else: + self.recordError(esError, &"Mismatched XML close tag ''", + ErrMismatchedBracket, tagStart, + hint = if self.tagStack.len > 0: &"Expected ''" else: "Unexpected close tag") + else: + self.tagStack.add(tagName) + self.emitToken(tkKeyword, tagName, tagStart) + else: + self.emitToken(tkOperator, "<", tagStart) + + # Read attributes until > or /> + while self.hasMore() and self.peek() != '>': + if self.peek() in {' ', '\t', '\n', '\r'}: + let wsStart2 = self.currentPos() + var ws2 = "" + while self.hasMore() and self.peek() in {' ', '\t', '\n', '\r'}: + ws2.add(self.advance()) + self.emitToken(tkWhitespace, ws2, wsStart2) + elif self.peek() == '/' and self.pos + 1 < self.sourceLen and self.peek(1) == '>': + isSelfClosing = true + discard self.advance() + discard self.advance() + if not isClosing and self.tagStack.len > 0: + discard self.tagStack.pop() + self.emitToken(tkOperator, "/>", tagStart) + break + elif isAlpha(self.peek()) or self.peek() in {'_', ':'}: + let attrStart = self.currentPos() + var attrName = "" + while self.hasMore() and (isAlphaNum(self.peek()) or self.peek() in {'_', ':', '-'}): + attrName.add(self.advance()) + self.emitToken(tkIdentifier, attrName, attrStart) + # Skip = "value" + while self.hasMore() and self.peek() in {' ', '\t', '\n', '\r'}: + discard self.advance() + if self.hasMore() and self.peek() == '=': + discard self.advance() + while self.hasMore() and self.peek() in {' ', '\t', '\n', '\r'}: + discard self.advance() + if self.hasMore() and self.peek() == '"': + let valStart = self.currentPos() + discard self.advance() + var val = "\"" + while self.hasMore() and self.peek() != '"': + val.add(self.advance()) + if self.hasMore(): + val.add(self.advance()) + self.emitToken(tkString, val, valStart) + else: + discard self.advance() + + if self.hasMore() and self.peek() == '>': + discard self.advance() + if not isClosing: + self.emitToken(tkOperator, ">", tagStart) + + elif c == '>': + discard self.advance() + # Already handled in tag context + + elif c == '"' or c == '\'': + let strStart = self.currentPos() + discard self.advance() + var content = $c + var foundClosing = false + while self.hasMore(): + let sc = self.advance() + if sc == c: + content.add(c) + foundClosing = true + break + content.add(sc) + if not foundClosing: + self.recordError(esError, &"Unclosed XML attribute value", ErrUnclosedString, strStart) + self.emitToken(tkString, content, strStart) + + elif isDigit(c) or (c == '.' and self.hasMore() and self.peek(1).isDigit()): + let numStart = self.currentPos() + var num = "" + while self.hasMore() and (self.peek().isDigit() or self.peek() in {'.', 'e', 'E', '+', '-'}): + num.add(self.advance()) + self.emitToken(tkNumber, num, numStart) + + elif isAlpha(c) or c == '_' or c == ':': + let idStart = self.currentPos() + var ident = "" + while self.hasMore() and (isAlphaNum(self.peek()) or self.peek() in {'_', ':', '-'}): + ident.add(self.advance()) + if xmlKeywordSet.contains(ident): + self.emitToken(tkKeyword, ident, idStart) + else: + self.emitToken(tkIdentifier, ident, idStart) + + elif c in {'&'}: + let entStart = self.currentPos() + var entity = "&" + discard self.advance() + while self.hasMore() and self.peek() != ';' and self.peek() != ' ': + entity.add(self.advance()) + if self.hasMore() and self.peek() == ';': + entity.add(self.advance()) + self.emitToken(tkSpecial, entity, entStart) + + elif c in {',', ';'}: + self.emitToken(tkPunctuation, $self.advance(), startPos) + + elif c in {'=', '!', '?'}: + self.emitToken(tkOperator, $self.advance(), startPos) + + else: + # Text content between tags + let txtStart = self.currentPos() + var text = "" + while self.hasMore() and self.peek() notin {'<', '&'}: + text.add(self.advance()) + if text.len > 0 and text.strip().len > 0: + self.emitToken(tkTemplateTag, text, txtStart) + elif text.len > 0: + self.emitToken(tkWhitespace, text, txtStart) + else: + discard self.advance() + + # Check for unclosed tags + for tag in self.tagStack: + let pos = newSourcePosition(self.line, self.column, self.pos) + self.recordError(esError, &"Unclosed XML tag '<{tag}>'", + ErrUnclosedBlock, pos, hint = &"Add closing ''") + + self.emitToken(tkEndOfFile, "", self.currentPos()) + when defined(nimcheckDebug): + if self.options.debugMode: debugLeave("XML_TOKENIZER", &"Complete: {self.tokens.len} tokens") diff --git a/tests/fixtures/c/edge_cases.c b/tests/fixtures/c/edge_cases.c new file mode 100644 index 0000000..5b59f48 --- /dev/null +++ b/tests/fixtures/c/edge_cases.c @@ -0,0 +1,40 @@ +#include +#include +#include +#include + +/* Unicode in comments: Привет, 世界, 🌍 */ + +int main(void) { + setlocale(LC_ALL, ""); + + /* Unicode string literals */ + wchar_t *greeting = L"こんにちは"; + wprintf(L"%ls\n", greeting); + + /* Null byte embedded */ + char buf[] = "Hello\x00World"; + printf("Length: %zu\n", strlen(buf)); /* stops at null byte */ + + /* Deeply nested structs */ + struct a { int x; }; + struct b { struct a a; }; + struct c { struct b b; }; + struct d { struct c c; }; + struct e { struct d d; }; + struct f { struct e e; }; + struct f val = {{{{{{42}}}}}}; + printf("%d\n", val.e.d.c.b.a.x); + + /* Very long function name */ + void this_is_an_extremely_long_function_name_that_goes_on_and_on_and_on(void) { + printf("deep\n"); + } + this_is_an_extremely_long_function_name_that_goes_on_and_on_and_on(); + + /* Encoding: BOM-like byte sequences */ + unsigned char raw[] = {0xEF, 0xBB, 0xBF, 'A', 'B', 'C', 0}; + printf("%s\n", raw); + + return 0; +} diff --git a/tests/fixtures/c/invalid.c b/tests/fixtures/c/invalid.c new file mode 100644 index 0000000..4185f79 --- /dev/null +++ b/tests/fixtures/c/invalid.c @@ -0,0 +1,26 @@ +#include +#include + +#define MAX 10 + +int main(void) { + int x = 5 + printf("x is %d\n", x) + + char *s = "unclosed string; + printf("%s\n", s); + + int arr[3] = {1, 2, 3; + printf("%d\n", arr[1]); + + if (x > 0 { + printf("positive\n"); + } + + char *p = malloc(10; + free(p); + + struct { int a; int b; } s = {1, 2}; + s.a = ; + + return 0; diff --git a/tests/fixtures/c/valid.c b/tests/fixtures/c/valid.c new file mode 100644 index 0000000..0bb3031 --- /dev/null +++ b/tests/fixtures/c/valid.c @@ -0,0 +1,36 @@ +#include +#include +#include + +#define MAX_NAME 64 +#define GREETING "Hello, World!" + +typedef struct { + int id; + char name[MAX_NAME]; + double score; +} Player; + +int add(int a, int b) { + return a + b; +} + +void greet(const char *name) { + printf("%s, %s!\n", GREETING, name); +} + +int main(void) { + Player p = {1, "Alice", 95.5}; + greet(p.name); + + int result = add(3, 4); + printf("3 + 4 = %d\n", result); + + FILE *fp = fopen("test.txt", "w"); + if (fp) { + fprintf(fp, "id=%d,name=%s,score=%.1f\n", p.id, p.name, p.score); + fclose(fp); + } + + return 0; +} diff --git a/tests/fixtures/cpp/edge_cases.cpp b/tests/fixtures/cpp/edge_cases.cpp new file mode 100644 index 0000000..119fa1e --- /dev/null +++ b/tests/fixtures/cpp/edge_cases.cpp @@ -0,0 +1,46 @@ +#include +#include +#include +#include +#include + +/* Unicode comment: 你好, 世界, 🌍🚀 */ + +int main() { + // Unicode string literals + std::string utf8 = "こんにちは世界"; + std::cout << utf8 << std::endl; + + // Null byte in string + std::string null_str = std::string("hello\x00world", 11); + std::cout << "Null-str size: " << null_str.size() << std::endl; + + // Deeply nested templates + template + struct Wrap { T value; }; + + Wrap>>>>>>> deep = {{{{{{{{42}}}}}}}}; + std::cout << deep.value.value.value.value.value.value.value.value << std::endl; + + // Extremely long string + std::string long_str(10000, 'x'); + std::cout << "Long string length: " << long_str.length() << std::endl; + + // BOM-like bytes + const unsigned char bom[] = {0xEF, 0xBB, 0xBF, 'H', 'i', 0}; + std::cout << reinterpret_cast(bom) << std::endl; + + // Deep lambda nesting + auto f1 = [](int x) { + return [x](int y) { + return [x, y](int z) { + return [x, y, z](int w) { + return x + y + z + w; + }; + }; + }; + }; + std::cout << "Nested lambdas: " << f1(1)(2)(3)(4) << std::endl; + + return 0; +} diff --git a/tests/fixtures/cpp/invalid.cpp b/tests/fixtures/cpp/invalid.cpp new file mode 100644 index 0000000..6661202 --- /dev/null +++ b/tests/fixtures/cpp/invalid.cpp @@ -0,0 +1,33 @@ +#include +#include + +class Broken { +public: + Broken(int x) : x_(x) {} + + void show() { + std::cout << x_ << std::endl; + + int missing_semicolon() + return x_; + } + +private: + int x_ +}; + +int main() { + Broken b(42); + b.show() + + char *s = "unclosed string; + std::cout << s << std::endl; + + if (b.x_ > 0 { + std::cout << "positive" << std::endl; + } + + std::vector v = {1, 2, 3}; + std::cout << v[5] << std::endl; + + return 0; diff --git a/tests/fixtures/cpp/valid.cpp b/tests/fixtures/cpp/valid.cpp new file mode 100644 index 0000000..5b047fa --- /dev/null +++ b/tests/fixtures/cpp/valid.cpp @@ -0,0 +1,58 @@ +#include +#include +#include +#include +#include + +class Person { +public: + Person(int id, std::string name) + : id_(id), name_(std::move(name)) {} + + int id() const { return id_; } + const std::string& name() const { return name_; } + + virtual void greet() const { + std::cout << "Hello, I'm " << name_ << std::endl; + } + + virtual ~Person() = default; + +private: + int id_; + std::string name_; +}; + +class Student : public Person { +public: + Student(int id, std::string name, double gpa) + : Person(id, std::move(name)), gpa_(gpa) {} + + void greet() const override { + std::cout << "Hi, I'm " << name() << " (GPA: " << gpa_ << ")" << std::endl; + } + +private: + double gpa_; +}; + +template +T max_value(T a, T b) { + return (a > b) ? a : b; +} + +int main() { + auto p = std::make_unique(1, "Alice", 3.9); + p->greet(); + + std::vector nums = {3, 1, 4, 1, 5, 9}; + std::sort(nums.begin(), nums.end()); + + for (int n : nums) { + std::cout << n << " "; + } + std::cout << std::endl; + + std::cout << "Max of 10 and 20: " << max_value(10, 20) << std::endl; + return 0; +} diff --git a/tests/fixtures/csharp/edge_cases.cs b/tests/fixtures/csharp/edge_cases.cs new file mode 100644 index 0000000..6529b06 --- /dev/null +++ b/tests/fixtures/csharp/edge_cases.cs @@ -0,0 +1,70 @@ +using System; +using System.Text; +using System.Collections.Generic; + +// Unicode comment: 你好世界 🌍🚀 + +public class EdgeCases +{ + public static void Main() + { + // Unicode string literals + string unicode = "こんにちは世界"; + Console.WriteLine(unicode); + + // Null byte embedded + string nullStr = "hello\x00world"; + Console.WriteLine($"Null-str length: {nullStr.Length}"); + + // Deeply nested generics + var deep = new Dictionary>>>> + { + { 1, new Dictionary>>> + { + { 2, new Dictionary>> + { + { 3, new Dictionary> + { + { 4, new Dictionary { { 5, "deep" } } } + } + } + } + } + } + } + }; + Console.WriteLine(deep[1][2][3][4][5]); + + // Very long string + string longStr = new string('x', 10000); + Console.WriteLine($"Long string length: {longStr.Length}"); + + // BOM prefix + byte[] bom = { 0xEF, 0xBB, 0xBF, (byte)'H', (byte)'i' }; + Console.WriteLine(Encoding.UTF8.GetString(bom)); + + // Deep exception nesting + try + { + try + { + try + { + throw new InvalidOperationException("inner"); + } + catch (Exception ex) + { + throw new ApplicationException("middle", ex); + } + } + catch (Exception ex) + { + throw new Exception("outer", ex); + } + } + catch (Exception ex) + { + Console.WriteLine($"Nested exception: {ex.Message}"); + } + } +} diff --git a/tests/fixtures/csharp/invalid.cs b/tests/fixtures/csharp/invalid.cs new file mode 100644 index 0000000..244527c --- /dev/null +++ b/tests/fixtures/csharp/invalid.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; + +namespace Broken +{ + public class BrokenClass + { + public int X { get; set; } + public string Name; + + public BrokenClass(int x, string name) + { + X = x; + Name = name + } + + public void Show() + { + Console.WriteLine($"{Name}: {X}") + } + + public int MissingBody() + } + + public class Derived : BrokenClass + { + public Derived() : base(42, "test") + { + } + + public void BrokenMethod() + { + string s = "unclosed string; + Console.WriteLine(s); + + if (X > 0 + Console.WriteLine("positive"); + } + + int[] arr = {1, 2, 3; + Console.WriteLine(arr[0]); + } + } + + public class Program + { + public static void Main() + { + var obj = new Derived(); + obj.Show() + } + } +} diff --git a/tests/fixtures/csharp/valid.cs b/tests/fixtures/csharp/valid.cs new file mode 100644 index 0000000..ff3ed7b --- /dev/null +++ b/tests/fixtures/csharp/valid.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Fixtures +{ + public class Person + { + public int Id { get; set; } + public string Name { get; set; } + public string Email { get; set; } + + public Person(int id, string name, string email) + { + Id = id; + Name = name; + Email = email; + } + + public override string ToString() + { + return $"{Name} <{Email}>"; + } + } + + public class Repository + { + private readonly Dictionary _items = new(); + + public void Add(int key, T item) + { + _items[key] = item; + } + + public T? Find(int key) + { + return _items.TryGetValue(key, out var item) ? item : default; + } + + public IEnumerable GetAll() + { + return _items.Values.ToList(); + } + } + + public static class Helpers + { + public static int Add(int a, int b) => a + b; + + public static IEnumerable Filter(IEnumerable items, Func predicate) + { + return items.Where(predicate); + } + } + + public class Program + { + public static void Main(string[] args) + { + var repo = new Repository(); + repo.Add(1, new Person(1, "Alice", "alice@example.com")); + repo.Add(2, new Person(2, "Bob", "bob@example.com")); + + var active = repo.GetAll(); + foreach (var p in active) + { + Console.WriteLine(p); + } + + Console.WriteLine($"3 + 4 = {Helpers.Add(3, 4)}"); + } + } +} diff --git a/tests/fixtures/css/edge_cases.css b/tests/fixtures/css/edge_cases.css new file mode 100644 index 0000000..dcbc61b --- /dev/null +++ b/tests/fixtures/css/edge_cases.css @@ -0,0 +1,39 @@ +:root { + --color-primary: #2563eb; +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +body { + font-family: "Inter", sans-serif; + background: var(--color-bg); + color: var(--color-text); +} + +.navbar { + display: flex; + background: linear-gradient(135deg, var(--color-primary), #7c3aed); +} + +.btn { + padding: 0.625rem 1.25rem; + border: none; + border-radius: 0.5rem; + cursor: pointer; + transition: background 0.2s ease; +} + +@keyframes slideIn { + from { opacity: 0; transform: translateX(-20px); } + to { opacity: 1; transform: translateX(0); } +} + +@media (max-width: 600px) { + .navbar { + flex-direction: column; + } +} diff --git a/tests/fixtures/css/invalid.css b/tests/fixtures/css/invalid.css new file mode 100644 index 0000000..45b9210 --- /dev/null +++ b/tests/fixtures/css/invalid.css @@ -0,0 +1,28 @@ +:root { + --color-primary: #2563eb; +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +body { + font-family: "Inter", sans-serif; +} + +.navbar { + display: flex; + background: linear-gradient(135deg, var(--color-primary), #7c3aed); +} + +.btn { + padding: 0.625rem 1.25rem; + border: none; +} + +@media (max-width: 600px) { + .navbar + flex-direction: column; +} diff --git a/tests/fixtures/css/valid.css b/tests/fixtures/css/valid.css new file mode 100644 index 0000000..73015b3 --- /dev/null +++ b/tests/fixtures/css/valid.css @@ -0,0 +1,159 @@ +:root { + --color-primary: #2563eb; + --color-primary-hover: #1d4ed8; + --color-bg: #f8fafc; + --color-text: #1e293b; + --color-border: #e2e8f0; + --radius-md: 0.5rem; + --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05); + --font-sans: "Inter", system-ui, -apple-system, sans-serif; +} + +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html { + font-size: 16px; + scroll-behavior: smooth; +} + +body { + font-family: var(--font-sans); + background: var(--color-bg); + color: var(--color-text); + line-height: 1.6; +} + +.container { + max-width: 1200px; + margin: 0 auto; + padding: 0 1rem; +} + +.navbar { + display: flex; + align-items: center; + justify-content: space-between; + padding: 1rem 2rem; + background: linear-gradient(135deg, var(--color-primary), #7c3aed); +} + +.navbar .logo { + font-size: 1.5rem; + font-weight: 700; + color: #fff; + text-decoration: none; +} + +.nav-links { + display: flex; + gap: 1.5rem; + list-style: none; +} + +.nav-links a { + color: rgb(255 255 255 / 0.85); + text-decoration: none; + transition: color 0.2s ease; +} + +.nav-links a:hover { + color: #fff; +} + +.card { + background: #fff; + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + box-shadow: var(--shadow-sm); + padding: 1.5rem; +} + +.card__title { + font-size: 1.25rem; + font-weight: 600; + margin-bottom: 0.75rem; +} + +.card__body { + color: #64748b; +} + +.btn { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.625rem 1.25rem; + border: none; + border-radius: var(--radius-md); + font-weight: 500; + cursor: pointer; + transition: background 0.2s ease, transform 0.1s ease; +} + +.btn:active { + transform: scale(0.97); +} + +.btn--primary { + background: var(--color-primary); + color: #fff; +} + +.btn--primary:hover { + background: var(--color-primary-hover); +} + +.btn--outline { + background: transparent; + border: 2px solid var(--color-primary); + color: var(--color-primary); +} + +.grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 1.5rem; +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: translateY(0); } +} + +.animate-in { + animation: fadeIn 0.4s ease-out both; +} + +@media (max-width: 768px) { + .navbar { + flex-direction: column; + gap: 1rem; + } + + .nav-links { + flex-wrap: wrap; + justify-content: center; + } + + .grid { + grid-template-columns: 1fr; + } +} + +@media (prefers-color-scheme: dark) { + :root { + --color-bg: #0f172a; + --color-text: #e2e8f0; + --color-border: #334155; + } + + .card { + background: #1e293b; + } +} diff --git a/tests/fixtures/dockerfile/edge_cases.Dockerfile b/tests/fixtures/dockerfile/edge_cases.Dockerfile new file mode 100644 index 0000000..516e877 --- /dev/null +++ b/tests/fixtures/dockerfile/edge_cases.Dockerfile @@ -0,0 +1,9 @@ +FROM alpine:3.20 as builder +RUN apk add --no-cache gcc musl-dev make +WORKDIR /build +COPY . . +RUN make release +FROM alpine:3.20 +RUN apk add --no-cache pcre openssl +COPY --from=builder /build/bin/app /usr/local/bin/app +ENTRYPOINT ["app"] diff --git a/tests/fixtures/dockerfile/invalid.Dockerfile b/tests/fixtures/dockerfile/invalid.Dockerfile new file mode 100644 index 0000000..af695c9 --- /dev/null +++ b/tests/fixtures/dockerfile/invalid.Dockerfile @@ -0,0 +1,12 @@ +FROM alpine:3.20 AS builder +RUN apk add --no-cache \ + gcc \ + musl-dev \ + make \ + +FROM alpine:3.20 + +COPY --from=builder /build/bin/app /usr/local/bin/app + +ENTRYPOINT ["app" +CMD ["--help"] diff --git a/tests/fixtures/dockerfile/valid.Dockerfile b/tests/fixtures/dockerfile/valid.Dockerfile new file mode 100644 index 0000000..96dcfb1 --- /dev/null +++ b/tests/fixtures/dockerfile/valid.Dockerfile @@ -0,0 +1,45 @@ +FROM alpine:3.20 AS builder + +RUN apk add --no-cache \ + gcc \ + musl-dev \ + pcre-dev \ + openssl-dev \ + make \ + git + +WORKDIR /build + +COPY . . + +RUN make release \ + && strip bin/nimcheck + +FROM alpine:3.20 AS runtime + +RUN apk add --no-cache \ + pcre \ + openssl \ + ca-certificates \ + tzdata + +ENV TZ=UTC +ENV NIMCHECK_LOG_LEVEL=info +ENV NIMCHECK_THREADS=4 + +RUN addgroup -g 1000 nimcheck \ + && adduser -D -u 1000 -G nimcheck nimcheck + +COPY --from=builder /build/bin/nimcheck /usr/local/bin/nimcheck + +RUN chmod 755 /usr/local/bin/nimcheck + +WORKDIR /workspace + +USER nimcheck + +HEALTHCHECK --interval=30s --timeout=3s --retries=3 \ + CMD nimcheck --version || exit 1 + +ENTRYPOINT ["nimcheck"] +CMD ["--help"] diff --git a/tests/fixtures/go/edge_cases.go b/tests/fixtures/go/edge_cases.go new file mode 100644 index 0000000..2674a7e --- /dev/null +++ b/tests/fixtures/go/edge_cases.go @@ -0,0 +1,181 @@ +package main + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "fmt" + "hash" + "io" + "math" + "net/url" + "reflect" + "sort" + "sync/atomic" + "time" + "unsafe" +) + +const ( + MaxRetries = 3 + BaseDelay = 100 * time.Millisecond + MaxDelay = 5 * time.Second +) + +type Executor[T any] struct { + workers int + tasks chan func() T + results chan T +} + +func NewExecutor[T any](workers int) *Executor[T] { + return &Executor[T]{ + workers: workers, + tasks: make(chan func() T, workers*2), + results: make(chan T, workers*2), + } +} + +func (e *Executor[T]) Submit(task func() T) { + e.tasks <- task +} + +func (e *Executor[T]) Run(ctx context.Context) []T { + for i := 0; i < e.workers; i++ { + go func() { + for { + select { + case <-ctx.Done(): + return + case task := <-e.tasks: + e.results <- task() + } + } + }() + } + + var results []T + for i := 0; i < cap(e.tasks); i++ { + select { + case r := <-e.results: + results = append(results, r) + case <-ctx.Done(): + return results + } + } + return results +} + +func retryWithBackoff(operation func() error) error { + var err error + delay := BaseDelay + + for i := 0; i < MaxRetries; i++ { + if err = operation(); err == nil { + return nil + } + if i < MaxRetries-1 { + jitter := time.Duration(rand.Int63n(int64(delay / 2))) + time.Sleep(delay + jitter) + delay = time.Duration(math.Min(float64(delay*2), float64(MaxDelay))) + } + } + return fmt.Errorf("operation failed after %d retries: %w", MaxRetries, err) +} + +func HashContent(r io.Reader) (string, error) { + h := sha256.New() + if _, err := io.Copy(h, r); err != nil { + return "", fmt.Errorf("hashing failed: %w", err) + } + return hex.EncodeToString(h.Sum(nil)), nil +} + +func SliceToMap[K comparable, V any](items []V, keyFn func(V) K) map[K]V { + result := make(map[K]V, len(items)) + for _, item := range items { + result[keyFn(item)] = item + } + return result +} + +func InverseMap[K, V comparable](m map[K]V) map[V]K { + result := make(map[V]K, len(m)) + for k, v := range m { + result[v] = k + } + return result +} + +var counter atomic.Int64 + +func nextID() int64 { + return counter.Add(1) +} + +type Enum interface { + ~int + String() string +} + +type Color int + +const ( + Red Color = iota + Green + Blue +) + +func (c Color) String() string { + return [...]string{"red", "green", "blue"}[c] +} + +func Must[T any](val T, err error) T { + if err != nil { + panic(err) + } + return val +} + +type TransformFunc[T, U any] func(T) U + +func Chain[T, U, V any](f TransformFunc[T, U], g TransformFunc[U, V]) TransformFunc[T, V] { + return func(t T) V { + return g(f(t)) + } +} + +func zero[T any]() T { + var zero T + return zero +} + +func Map[T, U any](items []T, fn func(T) U) []U { + result := make([]U, len(items)) + for i, item := range items { + result[i] = fn(item) + } + return result +} + +func Filter[T any](items []T, fn func(T) bool) []T { + var result []T + for _, item := range items { + if fn(item) { + result = append(result, item) + } + } + return result +} + +func Reduce[T, U any](items []T, init U, fn func(U, T) U) U { + acc := init + for _, item := range items { + acc = fn(acc, item) + } + return acc +} + +func init() { + fmt.Println("initializing package main") +} diff --git a/tests/fixtures/go/invalid.go b/tests/fixtures/go/invalid.go new file mode 100644 index 0000000..ef659d5 --- /dev/null +++ b/tests/fixtures/go/invalid.go @@ -0,0 +1,26 @@ +package main + +import "fmt" + +func main() { + x := 42 + fmt.Println(x + +func brokenFunc(x int) string { + return x +} + +type User struct { + Name string + Age int +} + +func (u User) Greet() string { + return "Hello, " + .Name + +switch x { +case 1: + fmt.Println("one") +case 2: + fmt.Println("two") +} diff --git a/tests/fixtures/go/valid.go b/tests/fixtures/go/valid.go new file mode 100644 index 0000000..7028cbe --- /dev/null +++ b/tests/fixtures/go/valid.go @@ -0,0 +1,149 @@ +package main + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "log" + "net/http" + "os" + "strings" + "sync" + "time" +) + +type User struct { + ID int `json:"id"` + Name string `json:"name"` + Email string `json:"email"` + Active bool `json:"active"` + CreatedAt time.Time `json:"created_at"` +} + +type UserService struct { + mu sync.RWMutex + users map[int]*User +} + +func NewUserService() *UserService { + return &UserService{ + users: make(map[int]*User), + } +} + +func (s *UserService) Add(user *User) error { + s.mu.Lock() + defer s.mu.Unlock() + + if user.Name == "" { + return errors.New("name is required") + } + if !strings.Contains(user.Email, "@") { + return errors.New("invalid email") + } + if _, exists := s.users[user.ID]; exists { + return errors.New("user already exists") + } + s.users[user.ID] = user + return nil +} + +func (s *UserService) FindByID(id int) (*User, error) { + s.mu.RLock() + defer s.mu.RUnlock() + + user, ok := s.users[id] + if !ok { + return nil, fmt.Errorf("user %d not found", id) + } + return user, nil +} + +func (s *UserService) FindByEmail(email string) *User { + s.mu.RLock() + defer s.mu.RUnlock() + + for _, u := range s.users { + if u.Email == email { + return u + } + } + return nil +} + +func (s *UserService) All() []*User { + s.mu.RLock() + defer s.mu.RUnlock() + + result := make([]*User, 0, len(s.users)) + for _, u := range s.users { + result = append(result, u) + } + return result +} + +func loggingMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + log.Printf("%s %s %s", r.Method, r.URL.Path, r.RemoteAddr) + next.ServeHTTP(w, r) + log.Printf("%s completed in %v", r.URL.Path, time.Since(start)) + }) +} + +func handleUsers(svc *UserService) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + switch r.Method { + case http.MethodGet: + users := svc.All() + json.NewEncoder(w).Encode(users) + + case http.MethodPost: + var user User + if err := json.NewDecoder(r.Body).Decode(&user); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + if err := svc.Add(&user); err != nil { + http.Error(w, err.Error(), http.StatusConflict) + return + } + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(user) + + default: + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + } + } +} + +func main() { + svc := NewUserService() + svc.Add(&User{ + ID: 1, + Name: "Alice", + Email: "alice@example.com", + Active: true, + }) + + mux := http.NewServeMux() + mux.HandleFunc("/users", handleUsers(svc)) + + port := os.Getenv("PORT") + if port == "" { + port = "8080" + } + + server := &http.Server{ + Addr: ":" + port, + Handler: loggingMiddleware(mux), + ReadTimeout: 10 * time.Second, + WriteTimeout: 10 * time.Second, + } + + log.Printf("Server starting on :%s", port) + log.Fatal(server.ListenAndServe()) +} diff --git a/tests/fixtures/java/edge_cases.java b/tests/fixtures/java/edge_cases.java new file mode 100644 index 0000000..50b3238 --- /dev/null +++ b/tests/fixtures/java/edge_cases.java @@ -0,0 +1,61 @@ +package fixtures; + +import java.util.*; +import java.nio.charset.*; + +// Unicode comment: 你好世界 🌍🚀 + +public class EdgeCases { + public static void main(String[] args) throws Exception { + // Unicode string literals + String unicode = "こんにちは世界"; + System.out.println(unicode); + + // Null byte in string + String nullStr = "hello\u0000world"; + System.out.println("Null-str length: " + nullStr.length()); + + // Deeply nested classes + Map>>>> deep = + new HashMap<>(); + Map>>> l4 = new HashMap<>(); + Map>> l3 = new HashMap<>(); + Map> l2 = new HashMap<>(); + Map l1 = new HashMap<>(); + l1.put(5, "deep"); + l2.put(4, l1); + l3.put(3, l2); + l4.put(2, l3); + deep.put(1, l4); + System.out.println(deep.get(1).get(2).get(3).get(4).get(5)); + + // Very long string + StringBuilder sb = new StringBuilder(10000); + for (int i = 0; i < 10000; i++) sb.append('x'); + System.out.println("Long string length: " + sb.length()); + + // BOM prefix + byte[] bom = {(byte)0xEF, (byte)0xBB, (byte)0xBF, (byte)'H', (byte)'i'}; + System.out.println(new String(bom, StandardCharsets.UTF_8)); + + // Deep exception chaining + try { + try { + try { + throw new RuntimeException("inner"); + } catch (Exception e) { + throw new RuntimeException("middle", e); + } + } catch (Exception e) { + throw new RuntimeException("outer", e); + } + } catch (Exception e) { + System.out.println("Chained: " + e.getMessage()); + } + + // Deep array nesting + int[][][][][][] deepArray = new int[2][2][2][2][2][2]; + deepArray[0][0][0][0][0][0] = 42; + System.out.println("Deep array: " + deepArray[0][0][0][0][0][0]); + } +} diff --git a/tests/fixtures/java/invalid.java b/tests/fixtures/java/invalid.java new file mode 100644 index 0000000..266ea24 --- /dev/null +++ b/tests/fixtures/java/invalid.java @@ -0,0 +1,44 @@ +package fixtures; + +import java.util.*; + +public class Invalid { + private int x; + + public Invalid(int x) { + this.x = x + } + + public void show() { + System.out.println(x) + } + + public String broken() { + String s = "unclosed string; + return s; + + public int missingBody() + + public void badIf() { + if (x > 0 { + System.out.println("positive"); + } + } + + public void badArray() { + int[] arr = {1, 2, 3; + System.out.println(arr[0]); + } + + public void missingSemicolon() { + int a = 5 + int b = 10; + int c = a + b + System.out.println(c); + } + + public static void main(String[] args) { + Invalid obj = new Invalid(42); + obj.show() + } +} diff --git a/tests/fixtures/java/valid.java b/tests/fixtures/java/valid.java new file mode 100644 index 0000000..010e1bc --- /dev/null +++ b/tests/fixtures/java/valid.java @@ -0,0 +1,59 @@ +package fixtures; + +import java.util.*; +import java.util.stream.*; + +public class Valid { + public static class Person { + private final int id; + private final String name; + private final String email; + + public Person(int id, String name, String email) { + this.id = id; + this.name = name; + this.email = email; + } + + public int getId() { return id; } + public String getName() { return name; } + public String getEmail() { return email; } + + @Override + public String toString() { + return name + " <" + email + ">"; + } + } + + public static class Repository { + private final Map items = new HashMap<>(); + + public void add(int key, T item) { + items.put(key, item); + } + + public Optional find(int key) { + return Optional.ofNullable(items.get(key)); + } + + public List getAll() { + return new ArrayList<>(items.values()); + } + } + + public static int add(int a, int b) { + return a + b; + } + + public static void main(String[] args) { + Repository repo = new Repository<>(); + repo.add(1, new Person(1, "Alice", "alice@example.com")); + repo.add(2, new Person(2, "Bob", "bob@example.com")); + + repo.getAll().stream() + .map(Person::getName) + .forEach(System.out::println); + + System.out.println("3 + 4 = " + add(3, 4)); + } +} diff --git a/tests/fixtures/kotlin/edge_cases.kt b/tests/fixtures/kotlin/edge_cases.kt new file mode 100644 index 0000000..fb96eb1 --- /dev/null +++ b/tests/fixtures/kotlin/edge_cases.kt @@ -0,0 +1,59 @@ +package fixtures + +// Unicode comment: 你好世界 🌍🚀 + +fun main() { + // Unicode string literals + val unicode = "こんにちは世界" + println(unicode) + + // Null byte in string + val nullStr = "hello\u0000world" + println("Null-str length: ${nullStr.length}") + + // Deeply nested generics + val deep = mapOf( + 1 to mapOf( + 2 to mapOf( + 3 to mapOf( + 4 to mapOf( + 5 to "deep" + ) + ) + ) + ) + ) + println(deep[1]?.get(2)?.get(3)?.get(4)?.get(5)) + + // Very long string + val longStr = "x".repeat(10000) + println("Long string length: ${longStr.length}") + + // BOM bytes + val bom = byteArrayOf(0xEF.toByte(), 0xBB.toByte(), 0xBF.toByte(), 'H'.code.toByte(), 'i'.code.toByte()) + println(String(bom)) + + // Deep lambda nesting + val f1: (Int) -> (Int) -> (Int) -> (Int) -> Int = { x -> + { y -> + { z -> + { w -> x + y + z + w } + } + } + } + println("Nested lambdas: ${f1(1)(2)(3)(4)}") + + // Infinite sequence (lazy) + val naturals = generateSequence(0) { it + 1 } + println("First 5 naturals: ${naturals.take(5).toList()}") + + // Deep data class nesting + data class A(val x: Int) + data class B(val a: A) + data class C(val b: B) + data class D(val c: C) + data class E(val d: D) + data class F(val e: E) + val deepData = F(E(D(C(B(A(42)))))) + println("Deep data: ${deepData.e.d.c.b.a.x}") +} diff --git a/tests/fixtures/kotlin/invalid.kt b/tests/fixtures/kotlin/invalid.kt new file mode 100644 index 0000000..c47a002 --- /dev/null +++ b/tests/fixtures/kotlin/invalid.kt @@ -0,0 +1,44 @@ +package fixtures + +data class Broken( + val x: Int, + val name: String +) + +fun show() { + println("hello") + val x = 5 + println(x) +} + +class BadRepo { + private val items = mutableMapOf() + + fun add(key: Int, item: T) { + items[key] = item + } + + fun find(key: Int): T? = items[key] +} + +fun main() { + val repo = BadRepo() + repo.add(1, "test") + + val s = "unclosed string; + println(s) + + val x = 5 + if (x > 0 { + println("positive") + } + + val list = listOf(1, 2, 3 + println(list) + + fun missingReturn(): Int { + val a = 5 + } + + println("done") +} diff --git a/tests/fixtures/kotlin/valid.kt b/tests/fixtures/kotlin/valid.kt new file mode 100644 index 0000000..2aa2a0d --- /dev/null +++ b/tests/fixtures/kotlin/valid.kt @@ -0,0 +1,38 @@ +package fixtures + +data class Person( + val id: Int, + val name: String, + val email: String +) + +class Repository { + private val items = mutableMapOf() + + fun add(key: Int, item: T) { + items[key] = item + } + + fun find(key: Int): T? = items[key] + + fun getAll(): List = items.values.toList() +} + +fun add(a: Int, b: Int): Int = a + b + +fun filter(items: List, predicate: (T) -> Boolean): List { + return items.filter(predicate) +} + +fun main() { + val repo = Repository() + repo.add(1, Person(1, "Alice", "alice@example.com")) + repo.add(2, Person(2, "Bob", "bob@example.com")) + + repo.getAll().forEach { println(it) } + println("3 + 4 = ${add(3, 4)}") + + val numbers = listOf(1, 2, 3, 4, 5) + val evens = filter(numbers) { it % 2 == 0 } + println("Evens: $evens") +} diff --git a/tests/fixtures/lua/edge_cases.lua b/tests/fixtures/lua/edge_cases.lua new file mode 100644 index 0000000..14e3dcc --- /dev/null +++ b/tests/fixtures/lua/edge_cases.lua @@ -0,0 +1,64 @@ +-- Unicode comment: 你好世界 🌍🚀 + +-- Unicode string literals +local unicode = "こんにちは世界" +print(unicode) + +-- Null byte in string +local null_str = "hello\0world" +print("Null-str length: " .. #null_str) + +-- Deeply nested tables +local deep = { + a = { + b = { + c = { + d = { + e = "deep" + } + } + } + } +} +print(deep.a.b.c.d.e) + +-- Very long string +local long_str = string.rep("x", 10000) +print("Long string length: " .. #long_str) + +-- Metatable chains +local a = {} +local b = {} +local c = {} +setmetatable(a, {__index = b}) +setmetatable(b, {__index = c}) +c.value = 42 +print("Metatable chain: " .. a.value) + +-- Deep function nesting +local function level1() + return function() + return function() + return function() + return function() + return 42 + end + end + end + end +end +print("Deep functions: " .. level1()()()()()) + +-- Coroutine with deep stack +local co = coroutine.create(function() + local function recurse(n) + if n == 0 then + coroutine.yield("done") + else + return recurse(n - 1) + end + end + recurse(100) +end) +local _, msg = coroutine.resume(co) +print("Coroutine: " .. msg) diff --git a/tests/fixtures/lua/invalid.lua b/tests/fixtures/lua/invalid.lua new file mode 100644 index 0000000..9d33ab1 --- /dev/null +++ b/tests/fixtures/lua/invalid.lua @@ -0,0 +1,31 @@ +local function broken() + local x = 5 + print(x) +end + +local s = "unclosed string +print(s) + +local function bad_if(x) + if x > 0 + print("positive") + end +end + +local function missing_end(x) + if x > 0 then + print("positive") +end + +local t = {1, 2, 3 +print(t[1]) + +local function nested() + local function inner() + local function deeper() + return 42 + end + end +end + +print("done" diff --git a/tests/fixtures/lua/valid.lua b/tests/fixtures/lua/valid.lua new file mode 100644 index 0000000..dfaf384 --- /dev/null +++ b/tests/fixtures/lua/valid.lua @@ -0,0 +1,57 @@ +local function add(a, b) + return a + b +end + +local function greet(name) + print("Hello, " .. name .. "!") +end + +local Person = {} +Person.__index = Person + +function Person:new(id, name, email) + local obj = { + id = id, + name = name, + email = email + } + setmetatable(obj, self) + return obj +end + +function Person:__tostring() + return self.name .. " <" .. self.email .. ">" +end + +local Repository = {} +Repository.__index = Repository + +function Repository:new() + return setmetatable({items = {}}, self) +end + +function Repository:add(key, item) + self.items[key] = item +end + +function Repository:find(key) + return self.items[key] +end + +function Repository:getAll() + local result = {} + for _, v in pairs(self.items) do + table.insert(result, v) + end + return result +end + +local repo = Repository:new() +repo:add(1, Person:new(1, "Alice", "alice@example.com")) +repo:add(2, Person:new(2, "Bob", "bob@example.com")) + +for _, p in ipairs(repo:getAll()) do + print(p) +end + +print("3 + 4 = " .. add(3, 4)) diff --git a/tests/fixtures/makefile/edge_cases.Makefile b/tests/fixtures/makefile/edge_cases.Makefile new file mode 100644 index 0000000..5e716bd --- /dev/null +++ b/tests/fixtures/makefile/edge_cases.Makefile @@ -0,0 +1,15 @@ +.PHONY: all clean + +CC := gcc +CFLAGS := -O2 -Wall + +all: program + +program: main.o util.o + $(CC) $(CFLAGS) -o $@ $^ + +%.o: %.c + $(CC) $(CFLAGS) -c -o $@ $< + +clean: + rm -f program *.o diff --git a/tests/fixtures/makefile/invalid.Makefile b/tests/fixtures/makefile/invalid.Makefile new file mode 100644 index 0000000..56d05cf --- /dev/null +++ b/tests/fixtures/makefile/invalid.Makefile @@ -0,0 +1,16 @@ +.PHONY: all clean + +CC := gcc +CFLAGS := -O2 -Wall + +all: program + +program: main.o util.o + $(CC) $(CFLAGS) -o $@ + +%.o: %.c + $(CC) $(CFLAGS) -c -o $< + echo "unclosed + +clean: + rm -f program *.o diff --git a/tests/fixtures/makefile/valid.Makefile b/tests/fixtures/makefile/valid.Makefile new file mode 100644 index 0000000..7407b1f --- /dev/null +++ b/tests/fixtures/makefile/valid.Makefile @@ -0,0 +1,57 @@ +.PHONY: all build release debug clean test coverage lint install uninstall + +NIM := nim +NIM_FLAGS := --hints:off --warnings:off +SRC_DIR := src +BIN_DIR := bin +TEST_DIR := tests +BUILD_DIR := build + +SOURCES := $(shell find $(SRC_DIR) -name '*.nim') +TESTS := $(shell find $(TEST_DIR) -name '*.nim') +OBJECTS := $(patsubst $(SRC_DIR)/%.nim,$(BUILD_DIR)/%.o,$(SOURCES)) + +BINARY := $(BIN_DIR)/nimcheck + +all: build + +$(BIN_DIR): + mkdir -p $(BIN_DIR) + +$(BUILD_DIR): + mkdir -p $(BUILD_DIR) + +$(BUILD_DIR)/%.o: $(SRC_DIR)/%.nim | $(BUILD_DIR) + $(NIM) compile --compileOnly -d:release -o:$@ $< + +$(BINARY): $(OBJECTS) | $(BIN_DIR) + $(NIM) compile -d:release --out:$(BINARY) $(SRC_DIR)/nimcheck.nim + +build: $(BINARY) + +release: + $(NIM) compile -d:release --opt:speed --passC:-flto --passL:-flto \ + --out:$(BINARY) $(SRC_DIR)/nimcheck.nim + +debug: + $(NIM) compile -d:debug --lineDir:on --stacktrace:on \ + --out:$(BINARY) $(SRC_DIR)/nimcheck.nim + +test: build + $(NIM) compile --run $(TEST_DIR)/test_all.nim + +coverage: + $(NIM) compile --run -d:coverage $(TEST_DIR)/test_all.nim + +lint: + $(NIM) check $(SRC_DIR)/nimcheck.nim + +clean: + rm -rf $(BIN_DIR) $(BUILD_DIR) + find . -name 'nimcache' -type d -exec rm -rf {} + 2>/dev/null || true + +install: release + install -m 755 $(BINARY) /usr/local/bin/nimcheck + +uninstall: + rm -f /usr/local/bin/nimcheck diff --git a/tests/fixtures/markdown/edge_cases.md b/tests/fixtures/markdown/edge_cases.md new file mode 100644 index 0000000..69cc7b8 --- /dev/null +++ b/tests/fixtures/markdown/edge_cases.md @@ -0,0 +1,30 @@ +# Nimcheck + +--- + +> A fast syntax validator. + +## Code Block + +```nim +proc hello(): string = + result = "Hello" +``` + +```python +def hello() -> str: + return "Hello" +``` + +| A | B | C | +|---|---|---| +| 1 | 2 | 3 | + +- [x] Done +- [ ] Pending + +~~strikethrough~~ + +Footnotes[^1] + +[^1]: A footnote. diff --git a/tests/fixtures/markdown/invalid.md b/tests/fixtures/markdown/invalid.md new file mode 100644 index 0000000..a93577b --- /dev/null +++ b/tests/fixtures/markdown/invalid.md @@ -0,0 +1,26 @@ +# Nimcheck + +> A fast syntax validator + +## Features + +- Fast +- Accurate +- 27+ languages + +## Unclosed Code Block + +```nim +proc hello(): string = + result = "Hello" + +Still in code block without closing fence. + +## Broken Link + +[Broken]( + +## Broken Table + +| A | B | C +| 1 | 2 | 3 | diff --git a/tests/fixtures/markdown/valid.md b/tests/fixtures/markdown/valid.md new file mode 100644 index 0000000..6b24e44 --- /dev/null +++ b/tests/fixtures/markdown/valid.md @@ -0,0 +1,131 @@ +# Nimcheck: Universal Syntax Validator + +[![Nim](https://img.shields.io/badge/Nim-2.0+-yellow)](https://nim-lang.org) +[![License](https://img.shields.io/badge/license-MIT-blue)](LICENSE) + +> **Fast, accurate syntax validation for 27+ languages.** +> Built in Nim. Zero dependencies beyond the standard library. + +--- + +## Table of Contents + +1. [Features](#features) +2. [Installation](#installation) +3. [Quick Start](#quick-start) +4. [Usage](#usage) +5. [Supported Languages](#supported-languages) + +--- + +## Features + +- **Multi-language** — validates Nim, Python, JavaScript, TypeScript, PHP, Ruby, Rust, Go, Lua, C, C++, C#, Java, Swift, Kotlin, Bash, HTML, XML, Jinja, JSON, YAML, TOML, CSS, SQL, Markdown, Dockerfile, Makefile +- **Blazing fast** — compiles to native code via Nim +- **Auto-detection** — identifies language from extension, shebang, or content analysis +- **CI-friendly** — exit codes and structured output + +### Detection Methods + +| Priority | Method | Confidence | +|----------|-------------|------------| +| 1 | Explicit | 100% | +| 2 | Extension | 90% | +| 3 | Shebang | 95% | +| 4 | Content | 70-95% | + +--- + +## Installation + +### From Source + +```bash +git clone https://github.com/retoor/nimcheck +cd nimcheck +make release +``` + +### Via Nimble + +```bash +nimble install nimcheck +``` + +--- + +## Quick Start + +```bash +nimcheck src/ # validate all files in src/ +nimcheck --flavor nim # validate as Nim +nimcheck --json # JSON output +nimcheck --watch # watch mode +``` + +--- + +## Usage Examples + +### Validate a single file + +```bash +nimcheck src/main.nim +``` + +### Validate with explicit language + +```bash +nimcheck --flavor python script.py +``` + +### Recursive directory scan + +```bash +nimcheck --recursive project/ +``` + +### JSON output for CI + +```bash +nimcheck --json --recursive src/ > report.json +``` + +--- + +## Supported Languages + +| Language | Extensions | +|-------------|-------------------------------------| +| Nim | `.nim`, `.nims`, `.nimble` | +| Python | `.py`, `.pyw`, `.pyx`, `.pxd` | +| JavaScript | `.js`, `.mjs`, `.cjs` | +| TypeScript | `.ts`, `.tsx` | +| PHP | `.php`, `.phtml` | +| HTML | `.html`, `.htm`, `.xhtml` | +| XML | `.xml`, `.svg` | +| JSON | `.json`, `.jsonc` | +| YAML | `.yaml`, `.yml` | +| TOML | `.toml` | +| CSS | `.css` | +| SQL | `.sql` | +| Markdown | `.md`, `.markdown` | +| Ruby | `.rb` | +| Rust | `.rs` | +| Go | `.go` | +| Lua | `.lua` | +| C | `.c`, `.h` | +| C++ | `.cpp`, `.cxx`, `.hpp` | +| C# | `.cs` | +| Java | `.java` | +| Swift | `.swift` | +| Kotlin | `.kt`, `.kts` | +| Bash | `.sh`, `.bash` | +| Dockerfile | `Dockerfile` | +| Makefile | `Makefile` | + +--- + +## License + +MIT © [Retoor](https://github.com/retoor) diff --git a/tests/fixtures/ruby/edge_cases.rb b/tests/fixtures/ruby/edge_cases.rb new file mode 100644 index 0000000..6f67b83 --- /dev/null +++ b/tests/fixtures/ruby/edge_cases.rb @@ -0,0 +1,66 @@ +module EnumerableExtensions + refine Array do + def second + self[1] + end + + def middle + self[size / 2] + end + end +end + +using EnumerableExtensions + +class Singleton + private_class_method :new + + @instance = nil + + def self.instance + @instance ||= new + end +end + +module_function + +def log(level, message) + warn "[#{level.upcase}] #{message}" +end + +require "ostruct" +require "set" +require "delegate" + +Point = Struct.new(:x, :y, keyword_init: true) +p = Point.new(x: 10, y: 20) + +class Stack + include Enumerable + + def initialize + @items = [] + end + + def push(item) + @items.push(item) + end + + def pop + @items.pop + end + + def each(&block) + @items.each(&block) + end +end + +s = Stack.new +s.push(1) +s.push(2) +s.each { |i| puts i } + +$global_count = 0 +DEFAULT_TIMEOUT = 30 + +puts "Global: #{$global_count}, Timeout: #{DEFAULT_TIMEOUT}" diff --git a/tests/fixtures/ruby/invalid.rb b/tests/fixtures/ruby/invalid.rb new file mode 100644 index 0000000..9b698b8 --- /dev/null +++ b/tests/fixtures/ruby/invalid.rb @@ -0,0 +1,12 @@ +class Broken + attr_accessor :x + + def missing_end + if x > 0 + puts "positive" + + def wrong_syntax + x = 42 + {x + 1 + +end diff --git a/tests/fixtures/ruby/valid.rb b/tests/fixtures/ruby/valid.rb new file mode 100644 index 0000000..1ccbcd9 --- /dev/null +++ b/tests/fixtures/ruby/valid.rb @@ -0,0 +1,80 @@ +#!/usr/bin/env ruby +require "json" +require "net/http" +require "uri" + +class User + attr_accessor :id, :name, :email, :active + + def initialize(id:, name:, email:, active: true) + @id = id + @name = name + @email = email + @active = active + end + + def to_s + "#{@name} <#{@email}>" + end + + def active? + @active + end + + def deactivate! + @active = false + end +end + +class UserRepository + def initialize + @users = {} + end + + def add(user) + @users[user.id] = user + end + + def find(id) + @users[id] + end + + def find_by_email(email) + @users.values.find { |u| u.email == email } + end + + def active_users + @users.values.select(&:active?) + end + + def each(&block) + @users.values.each(&block) + end + + def to_json(*args) + @users.values.map(&:to_h).to_json(*args) + end +end + +def fetch_users(url) + uri = URI.parse(url) + response = Net::HTTP.get_response(uri) + raise "HTTP error: #{response.code}" unless response.is_a?(Net::HTTPSuccess) + JSON.parse(response.body) +rescue StandardError => e + warn "Failed to fetch users: #{e.message}" + [] +end + +def process_items(items) + items.map { |item| yield item } +end + +repo = UserRepository.new +repo.add(User.new(id: 1, name: "Alice", email: "alice@example.com")) + +repo.each do |user| + puts user.to_s +end + +puts "Active: #{repo.active_users.size}" diff --git a/tests/fixtures/rust/edge_cases.rs b/tests/fixtures/rust/edge_cases.rs new file mode 100644 index 0000000..573659f --- /dev/null +++ b/tests/fixtures/rust/edge_cases.rs @@ -0,0 +1,147 @@ +use std::collections::{HashMap, HashSet, BTreeMap}; +use std::rc::Rc; +use std::cell::RefCell; +use std::sync::{Arc, Mutex}; +use std::marker::PhantomData; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Status { + Active, + Inactive, + Pending, + Banned, +} + +#[derive(Debug)] +pub struct TreeNode { + pub value: T, + pub left: Option>>, + pub right: Option>>, +} + +impl TreeNode { + pub fn insert(&mut self, value: T) { + let target = if value <= self.value { + &mut self.left + } else { + &mut self.right + }; + match target { + Some(node) => node.insert(value), + None => *target = Some(Box::new(TreeNode { + value, + left: None, + right: None, + })), + } + } + + pub fn contains(&self, value: &T) -> bool { + &self.value == value + || self.left.as_ref().map_or(false, |n| n.contains(value)) + || self.right.as_ref().map_or(false, |n| n.contains(value)) + } +} + +pub trait Monoid { + fn empty() -> Self; + fn combine(&self, other: &Self) -> Self; +} + +impl Monoid for i32 { + fn empty() -> Self { 0 } + fn combine(&self, other: &Self) -> Self { self + other } +} + +impl Monoid for String { + fn empty() -> Self { String::new() } + fn combine(&self, other: &Self) -> Self { format!("{}{}", self, other) } +} + +pub fn reduce(items: &[T]) -> T { + items.iter().fold(T::empty(), |acc, x| acc.combine(x)) +} + +macro_rules! vec_of_strings { + ($($x:expr),*) => (vec![$($x.to_string()),*]); +} + +pub struct Lazy T> +where + F: Fn() -> T, +{ + value: Option, + factory: F, +} + +impl T> Lazy { + pub fn new(factory: F) -> Self { + Self { + value: None, + factory, + } + } + + pub fn get(&mut self) -> &T { + self.value.get_or_insert_with(&self.factory) + } +} + +pub async fn parallel_map(items: Vec, f: F) -> Vec +where + T: Send + 'static, + U: Send + 'static, + F: Fn(T) -> U + Send + Sync + 'static, +{ + use tokio::task; + let mut handles = Vec::new(); + for item in items { + handles.push(task::spawn(async move { f(item) })); + } + let mut results = Vec::new(); + for handle in handles { + results.push(handle.await.unwrap()); + } + results +} + +pub struct CustomIterator { + items: Vec, + index: usize, +} + +impl Iterator for CustomIterator { + type Item = T; + fn next(&mut self) -> Option { + if self.index < self.items.len() { + let item = self.items[self.index].clone(); + self.index += 1; + Some(item) + } else { + None + } + } +} + +const DEFAULT_PORT: u16 = 8080; +static APP_NAME: &str = "nimcheck-rs"; +thread_local! { + static CACHE: RefCell> = RefCell::new(HashMap::new()); +} + +unsafe impl Send for User {} +unsafe impl Sync for User {} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_insert() { + let mut root = TreeNode { value: 5, left: None, right: None }; + root.insert(3); + root.insert(7); + assert!(root.contains(&3)); + assert!(root.contains(&7)); + } +} diff --git a/tests/fixtures/rust/invalid.rs b/tests/fixtures/rust/invalid.rs new file mode 100644 index 0000000..2b279c7 --- /dev/null +++ b/tests/fixtures/rust/invalid.rs @@ -0,0 +1,24 @@ +fn main() { + let x = 42 + let y = vec![1, 2, + println!("{}", x + +fn missing_type(x) -> i32 { + x +} + +match x { + 1 => println!("one"), + 2 => println!("two"), +} + +struct Point { + x: i32, + y: i32, +} + +impl Point { + fn new(x: i32, y: i32 -> Self { + Self { x, y } + } +} diff --git a/tests/fixtures/rust/valid.rs b/tests/fixtures/rust/valid.rs new file mode 100644 index 0000000..b465cb5 --- /dev/null +++ b/tests/fixtures/rust/valid.rs @@ -0,0 +1,112 @@ +use std::collections::HashMap; +use std::fmt::{Debug, Display}; +use std::str::FromStr; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct User { + pub id: u64, + pub name: String, + pub email: String, + pub active: bool, + pub tags: Vec, +} + +impl User { + pub fn new(id: u64, name: &str, email: &str) -> Self { + Self { + id, + name: name.to_string(), + email: email.to_string(), + active: true, + tags: Vec::new(), + } + } + + pub fn deactivate(&mut self) { + self.active = false; + } + + pub fn add_tag(&mut self, tag: &str) { + self.tags.push(tag.to_string()); + } +} + +impl Display for User { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{} <{}> [{}]", self.name, self.email, self.id) + } +} + +pub enum ApiError { + NotFound(String), + Unauthorized, + Internal(String), +} + +impl Debug for ApiError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ApiError::NotFound(msg) => write!(f, "NotFound({})", msg), + ApiError::Unauthorized => write!(f, "Unauthorized"), + ApiError::Internal(msg) => write!(f, "Internal({})", msg), + } + } +} + +pub type Result = std::result::Result; + +pub trait Repository { + fn find_by_id(&self, id: u64) -> Result; + fn save(&mut self, item: T) -> Result<()>; + fn delete(&mut self, id: u64) -> Result<()>; +} + +pub struct UserRepository { + users: HashMap, +} + +impl UserRepository { + pub fn new() -> Self { + Self { + users: HashMap::new(), + } + } +} + +impl Repository for UserRepository { + fn find_by_id(&self, id: u64) -> Result { + self.users + .get(&id) + .cloned() + .ok_or_else(|| ApiError::NotFound(format!("User {}", id))) + } + + fn save(&mut self, user: User) -> Result<()> { + let id = user.id; + self.users.insert(id, user); + Ok(()) + } + + fn delete(&mut self, id: u64) -> Result<()> { + self.users.remove(&id); + Ok(()) + } +} + +fn process_users(repo: &UserRepository) -> Result> { + let names: Vec = repo + .users + .values() + .filter(|u| u.active) + .map(|u| u.name.clone()) + .collect(); + Ok(names) +} + +fn main() -> Result<()> { + let mut repo = UserRepository::new(); + repo.save(User::new(1, "Alice", "alice@example.com"))?; + let names = process_users(&repo)?; + println!("{:?}", names); + Ok(()) +} diff --git a/tests/fixtures/sql/edge_cases.sql b/tests/fixtures/sql/edge_cases.sql new file mode 100644 index 0000000..9a7b85c --- /dev/null +++ b/tests/fixtures/sql/edge_cases.sql @@ -0,0 +1,62 @@ +CREATE TABLE users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username VARCHAR(64) NOT NULL UNIQUE, + email VARCHAR(255) NOT NULL UNIQUE, + password CHAR(64) NOT NULL, + is_active BOOLEAN NOT NULL DEFAULT TRUE, + role VARCHAR(32) NOT NULL DEFAULT 'user', + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE posts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + author_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + title VARCHAR(200) NOT NULL, + slug VARCHAR(200) NOT NULL UNIQUE, + body TEXT NOT NULL, + status VARCHAR(16) NOT NULL DEFAULT 'draft' + CHECK (status IN ('draft', 'published', 'archived')), + view_count INTEGER NOT NULL DEFAULT 0, + published_at TIMESTAMP, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +SELECT + u.username, + u.email, + p.title, + p.status, + p.view_count +FROM users u +JOIN posts p ON p.author_id = u.id +WHERE u.is_active = TRUE + AND p.status != 'archived' +ORDER BY p.created_at DESC +LIMIT 20; + +WITH cte AS ( + SELECT author_id, COUNT(*) AS cnt + FROM posts + GROUP BY author_id +) +SELECT u.username, cte.cnt +FROM users u +JOIN cte ON cte.author_id = u.id; + +UPDATE posts +SET status = 'archived', + updated_at = CURRENT_TIMESTAMP +WHERE published_at < datetime('now', '-1 year') + AND status = 'published'; + +SELECT + NULL AS maybe_value, + COALESCE(u.email, 'no-email') AS safe_email, + CASE + WHEN p.view_count > 1000 THEN 'popular' + WHEN p.view_count > 100 THEN 'moderate' + ELSE 'quiet' + END AS popularity +FROM users u +LEFT JOIN posts p ON p.author_id = u.id; diff --git a/tests/fixtures/sql/invalid.sql b/tests/fixtures/sql/invalid.sql new file mode 100644 index 0000000..c0b0acd --- /dev/null +++ b/tests/fixtures/sql/invalid.sql @@ -0,0 +1,30 @@ +CREATE TABLE users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username VARCHAR(64) NOT NULL UNIQUE, + email VARCHAR(255) NOT NULL UNIQUE, + password CHAR(64) NOT NULL, + is_active BOOLEAN NOT NULL DEFAULT TRUE, + role VARCHAR(32) NOT NULL DEFAULT 'user', + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, +); + +SELECT + u.username, + u.email, + p.title +FROM users u +JOIN posts p ON p.author_id = u.id +WHERE u.is_active = TRUE +ORDER BY p.created_at DESC +LIMIT 20 + +SELECT * FROM; + +CREATE INDEX ON posts(status); + +INSERT INTO users (username, email, password) +VALUES ('alice', 'alice@example.com'); + +UPDATE posts +SET status = 'archived', +WHERE published_at < datetime('now', '-1 year'); diff --git a/tests/fixtures/sql/valid.sql b/tests/fixtures/sql/valid.sql new file mode 100644 index 0000000..85d64f8 --- /dev/null +++ b/tests/fixtures/sql/valid.sql @@ -0,0 +1,91 @@ +CREATE TABLE users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username VARCHAR(64) NOT NULL UNIQUE, + email VARCHAR(255) NOT NULL UNIQUE, + password CHAR(64) NOT NULL, + is_active BOOLEAN NOT NULL DEFAULT TRUE, + role VARCHAR(32) NOT NULL DEFAULT 'user', + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE posts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + author_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + title VARCHAR(200) NOT NULL, + slug VARCHAR(200) NOT NULL UNIQUE, + body TEXT NOT NULL, + status VARCHAR(16) NOT NULL DEFAULT 'draft' + CHECK (status IN ('draft', 'published', 'archived')), + view_count INTEGER NOT NULL DEFAULT 0, + published_at TIMESTAMP, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE tags ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name VARCHAR(50) NOT NULL UNIQUE +); + +CREATE TABLE post_tags ( + post_id INTEGER NOT NULL REFERENCES posts(id) ON DELETE CASCADE, + tag_id INTEGER NOT NULL REFERENCES tags(id) ON DELETE CASCADE, + PRIMARY KEY (post_id, tag_id) +); + +CREATE INDEX idx_posts_author ON posts(author_id); +CREATE INDEX idx_posts_status ON posts(status); +CREATE INDEX idx_posts_slug ON posts(slug); +CREATE INDEX idx_users_email ON users(email); + +INSERT INTO users (username, email, password, role) +VALUES ('alice', 'alice@example.com', 'hashed_placeholder', 'admin'); + +INSERT INTO tags (name) VALUES ('nim'), ('parsing'), ('devtools'); + +INSERT INTO posts (author_id, title, slug, body, status) +VALUES (1, 'Introducing Nimcheck', 'introducing-nimcheck', + 'Nimcheck is a universal syntax validator.', 'published'); + +INSERT INTO post_tags (post_id, tag_id) +SELECT p.id, t.id +FROM posts p, tags t +WHERE p.slug = 'introducing-nimcheck' + AND t.name IN ('nim', 'parsing', 'devtools'); + +SELECT + u.username, + u.email, + p.title, + p.status, + p.view_count, + GROUP_CONCAT(t.name, ', ') AS tag_list +FROM users u +JOIN posts p ON p.author_id = u.id +LEFT JOIN post_tags pt ON pt.post_id = p.id +LEFT JOIN tags t ON t.id = pt.tag_id +WHERE u.is_active = TRUE + AND p.status != 'archived' +GROUP BY p.id +ORDER BY p.created_at DESC +LIMIT 20; + +UPDATE posts +SET status = 'archived', + updated_at = CURRENT_TIMESTAMP +WHERE published_at < datetime('now', '-1 year') + AND status = 'published'; + +WITH post_stats AS ( + SELECT + author_id, + COUNT(*) AS total_posts, + SUM(view_count) AS total_views, + AVG(view_count) AS avg_views + FROM posts + GROUP BY author_id +) +SELECT u.username, ps.total_posts, ps.total_views, ps.avg_views +FROM users u +JOIN post_stats ps ON ps.author_id = u.id +ORDER BY ps.total_views DESC; diff --git a/tests/fixtures/swift/edge_cases.swift b/tests/fixtures/swift/edge_cases.swift new file mode 100644 index 0000000..d63bf97 --- /dev/null +++ b/tests/fixtures/swift/edge_cases.swift @@ -0,0 +1,79 @@ +import Foundation + +// Unicode comment: 你好世界 🌍🚀 + +// Unicode string literals +let unicode = "こんにちは世界" +print(unicode) + +// Null byte in string +let nullStr = "hello\0world" +print("Null-str length: \(nullStr.count)") + +// Deeply nested optionals +let deep: Int????? = 42 +if let l1 = deep, + let l2 = l1, + let l3 = l2, + let l4 = l3 { + print("Deep optional: \(l4)") +} + +// Very long string +let longStr = String(repeating: "x", count: 10000) +print("Long string length: \(longStr.count)") + +// BOM bytes +let bom = Data([0xEF, 0xBB, 0xBF, 0x48, 0x69]) +print(String(data: bom, encoding: .utf8) ?? "") + +// Deep closure nesting +let f1: (Int) -> (Int) -> (Int) -> (Int) -> Int = { x in + { y in + { z in + { w in x + y + z + w } + } + } +} +print("Nested closures: \(f1(1)(2)(3)(4))") + +// Deep dictionary nesting +let deepDict: [String: Any] = [ + "a": [ + "b": [ + "c": [ + "d": [ + "e": "deep" + ] + ] + ] + ] +] +if let a = deepDict["a"] as? [String: Any], + let b = a["b"] as? [String: Any], + let c = b["c"] as? [String: Any], + let d = c["d"] as? [String: Any], + let e = d["e"] as? String { + print("Deep dict: \(e)") +} + +// Deep error nesting +enum AppError: Error { + case inner(String) + case middle(String, Error) + case outer(String, Error) +} + +do { + do { + do { + throw AppError.inner("oops") + } catch { + throw AppError.middle("wrapped", error) + } + } catch { + throw AppError.outer("re-wrapped", error) + } +} catch { + print("Nested error: \(error)") +} diff --git a/tests/fixtures/swift/invalid.swift b/tests/fixtures/swift/invalid.swift new file mode 100644 index 0000000..8e69c39 --- /dev/null +++ b/tests/fixtures/swift/invalid.swift @@ -0,0 +1,39 @@ +import Foundation + +struct Broken { + let x: Int + let name: String +} + +class BadRepo { + private var items: [Int: String] = [:] + + func add(key: Int, item: String) { + items[key] = item + } + + func show() { + print("hello") + let x = 5 + print(x) + } +} + +let s = "unclosed string +print(s) + +let x = 5 +if x > 0 { + print("positive") + +func missingReturn() -> Int { + let a = 5 +} + +let list = [1, 2, 3 +print(list) + +func badFunction() { + let y = 5 + print(y) +} diff --git a/tests/fixtures/swift/valid.swift b/tests/fixtures/swift/valid.swift new file mode 100644 index 0000000..1508b95 --- /dev/null +++ b/tests/fixtures/swift/valid.swift @@ -0,0 +1,50 @@ +import Foundation + +struct Person: Codable { + let id: Int + let name: String + let email: String + let active: Bool + + init(id: Int, name: String, email: String, active: Bool = true) { + self.id = id + self.name = name + self.email = email + self.active = active + } +} + +class Repository { + private var items: [Int: T] = [:] + + func add(key: Int, item: T) { + items[key] = item + } + + func find(key: Int) -> T? { + return items[key] + } + + func getAll() -> [T] { + return Array(items.values) + } +} + +func add(_ a: Int, _ b: Int) -> Int { + return a + b +} + +enum Result { + case success(T) + case failure(Error) +} + +let repo = Repository() +repo.add(key: 1, item: Person(id: 1, name: "Alice", email: "alice@example.com")) +repo.add(key: 2, item: Person(id: 2, name: "Bob", email: "bob@example.com")) + +for person in repo.getAll() { + print("\(person.name) <\(person.email)>") +} + +print("3 + 4 = \(add(3, 4))") diff --git a/tests/fixtures/typescript/edge_cases.ts b/tests/fixtures/typescript/edge_cases.ts new file mode 100644 index 0000000..c30c2bb --- /dev/null +++ b/tests/fixtures/typescript/edge_cases.ts @@ -0,0 +1,73 @@ +interface User { + id: number; + name: string; + email: string; + active: boolean; + metadata: Record; + tags: string[]; + createdAt: Date; +} + +type Result = { + success: boolean; + data?: T; + error?: string; + timestamp: number; +}; + +type DeepPartial = { + [K in keyof T]?: T[K] extends object ? DeepPartial : T[K]; +}; + +type Nullable = T | null | undefined; + +type UnionToIntersection = + (U extends unknown ? (k: U) => void : never) extends (k: infer I) => void ? I : never; + +class Container { + constructor(private value: T) {} + + map(fn: (val: T) => U): Container { + return new Container(fn(this.value)); + } + + flatMap(fn: (val: T) => Container): Container { + return fn(this.value); + } + + unwrap(): T { + return this.value; + } +} + +function pipe(...fns: Array<(arg: T) => T>): (arg: T) => T { + return (arg: T) => fns.reduce((acc, fn) => fn(acc), arg); +} + +function memoize unknown>(fn: T): T { + const cache = new Map(); + return ((...args: unknown[]) => { + const key = JSON.stringify(args); + if (cache.has(key)) return cache.get(key); + const result = fn(...args); + cache.set(key, result); + return result; + }) as unknown as T; +} + +function withDefault(value: T | undefined | null, fallback: T): T { + return value ?? fallback; +} + +async function* paginate(url: string, pageSize: number): AsyncGenerator { + let page = 0; + while (true) { + const response = await fetch(`${url}?page=${page}&size=${pageSize}`); + const items: T[] = await response.json(); + if (items.length === 0) return; + yield items; + page++; + } +} + +export { User, Result, Container, pipe, memoize, withDefault, paginate }; diff --git a/tests/fixtures/typescript/invalid.ts b/tests/fixtures/typescript/invalid.ts new file mode 100644 index 0000000..e2de077 --- /dev/null +++ b/tests/fixtures/typescript/invalid.ts @@ -0,0 +1,36 @@ +interface Broken { + id number; + name: ; + email string; +} + +type Result = { + success: boolean + data?: T + error: string +}; + +abstract class BadRepo> { + protected items: Map = new Map(); + + findById(id: number): T | undefined { + return this.items.get(id); + + validate(entity: T): boolean { + return true; + } +} + +async function fetchBad(url: string): Promise> { + const response = await fetch(url); + const data: User[[]] = await response.json(); + return { success: true, data }; +} + +const x: = 42; +const y = x as; + +function missingReturn(x: number): string { + if (x > 0) + return "positive" +} diff --git a/tests/fixtures/typescript/valid.ts b/tests/fixtures/typescript/valid.ts new file mode 100644 index 0000000..b81d958 --- /dev/null +++ b/tests/fixtures/typescript/valid.ts @@ -0,0 +1,86 @@ +interface User { + id: number; + name: string; + email: string; + active: boolean; + metadata: Record; + tags: string[]; + createdAt: Date; +} + +type Result = { + success: boolean; + data?: T; + error?: string; + timestamp: number; +}; + +enum LogLevel { + Debug = 0, + Info = 1, + Warn = 2, + Error = 3, +} + +abstract class BaseRepository { + protected items: Map = new Map(); + + findById(id: number): T | undefined { + return this.items.get(id); + } + + abstract validate(entity: T): boolean; + + save(entity: T): Result { + if (!this.validate(entity)) { + return { success: false, error: "Validation failed", timestamp: Date.now() }; + } + this.items.set(entity.id, entity); + return { success: true, data: entity, timestamp: Date.now() }; + } +} + +class UserRepository extends BaseRepository { + validate(user: User): boolean { + return user.name.length > 0 && user.email.includes("@"); + } + + findByEmail(email: string): User | undefined { + for (const user of this.items.values()) { + if (user.email === email) return user; + } + return undefined; + } +} + +async function fetchUsers(url: string): Promise> { + try { + const response = await fetch(url); + const data: User[] = await response.json(); + return { success: true, data, timestamp: Date.now() }; + } catch (err: unknown) { + const message = err instanceof Error ? err.message : "Unknown error"; + return { success: false, error: message, timestamp: Date.now() }; + } +} + +function processItems(items: T[], transform: (item: T) => U): U[] { + return items.map(transform); +} + +const repo = new UserRepository(); +const user: User = { + id: 1, + name: "Alice", + email: "alice@example.com", + active: true, + metadata: { role: "admin", level: 42 }, + tags: ["typescript", "backend"], + createdAt: new Date(), +}; + +repo.save(user); + +const names = processItems(repo.items.size > 0 ? Array.from(repo.items.values()) : [], (u) => u.name.toUpperCase()); + +export { User, UserRepository, fetchUsers, processItems, LogLevel }; diff --git a/tests/fixtures/xml/edge_cases.xml b/tests/fixtures/xml/edge_cases.xml new file mode 100644 index 0000000..cc6826d --- /dev/null +++ b/tests/fixtures/xml/edge_cases.xml @@ -0,0 +1,46 @@ + + + + + こんにちは世界🌍🚀 + + + hello�world + + + + + + + + + + + deep + + + + + + + + + + + xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + + + Hi + + + &<>"' + + + alert("xss")]]> + + + many-attrs + + + Some text with child and more text trailing. + diff --git a/tests/fixtures/xml/invalid.xml b/tests/fixtures/xml/invalid.xml new file mode 100644 index 0000000..38d32ae --- /dev/null +++ b/tests/fixtures/xml/invalid.xml @@ -0,0 +1,18 @@ + + + + Alice + alice@example.com + + + Bob + bob@example.com + true + + + value + &unknown; + + + + diff --git a/tests/fixtures/xml/valid.xml b/tests/fixtures/xml/valid.xml new file mode 100644 index 0000000..1dc145e --- /dev/null +++ b/tests/fixtures/xml/valid.xml @@ -0,0 +1,25 @@ + + + + Alice + alice@example.com + true + + admin + backend + + + + Bob + bob@example.com + false + + user + + + + test-fixture + 1.0 + 2026-07-13 + + diff --git a/tests/test_all.nim b/tests/test_all.nim index 8b7df0b..694ec0d 100644 --- a/tests/test_all.nim +++ b/tests/test_all.nim @@ -9,10 +9,14 @@ import ./test_python import ./test_python_exhaustive import ./test_javascript import ./test_javascript_exhaustive +import ./test_typescript +import ./test_typescript_exhaustive import ./test_php import ./test_php_exhaustive import ./test_html import ./test_html_exhaustive +import ./test_xml +import ./test_xml_exhaustive import ./test_jinja import ./test_jinja_exhaustive import ./test_json_exhaustive @@ -20,4 +24,34 @@ import ./test_yaml_exhaustive import ./test_toml_exhaustive import ./test_config import ./test_mixed +import ./test_c +import ./test_c_exhaustive +import ./test_cpp +import ./test_cpp_exhaustive +import ./test_java +import ./test_java_exhaustive +import ./test_csharp +import ./test_csharp_exhaustive +import ./test_kotlin +import ./test_kotlin_exhaustive +import ./test_lua +import ./test_lua_exhaustive +import ./test_swift +import ./test_swift_exhaustive +import ./test_go +import ./test_go_exhaustive +import ./test_rust +import ./test_rust_exhaustive +import ./test_ruby +import ./test_ruby_exhaustive +import ./test_css +import ./test_css_exhaustive +import ./test_sql +import ./test_sql_exhaustive +import ./test_markdown +import ./test_markdown_exhaustive +import ./test_dockerfile +import ./test_dockerfile_exhaustive +import ./test_makefile +import ./test_makefile_exhaustive import ./test_fuzz diff --git a/tests/test_c.nim b/tests/test_c.nim new file mode 100644 index 0000000..4cd1109 --- /dev/null +++ b/tests/test_c.nim @@ -0,0 +1,40 @@ +## C language validator tests. + +import std/[unittest, strutils, strformat] +import ../src/nimcheck +import test_common + +proc checkValid(result: ValidationResult, context: string = "") = + if result.errors.len > 0: + var msg = &"Expected valid, got {result.errors.len} error(s)" + if context.len > 0: msg.add(&" [{context}]") + for e in result.errors: msg.add(&"\n [{e.code}] {e.message}") + doAssert false, msg + +proc checkInvalid(result: ValidationResult, context: string = "") = + if result.errors.len == 0: + doAssert false, &"Expected errors, got none [{context}]" + +suite "C Validator": + test "valid C code": + let result = validateSource(CGoodCode, flavor = lfC) + checkValid(result) + + test "invalid C code returns errors": + let result = validateSource(CBadCode, flavor = lfC) + checkInvalid(result) + + test "valid .c file": + let result = validateFile("tests/fixtures/c/valid.c") + checkValid(result) + + test "invalid .c file returns errors": + let result = validateFile("tests/fixtures/c/invalid.c") + check result.errors.len > 0 + + test "detectFlavor detects C": + check $detectFlavor(CGoodCode) == "c" + + test "inspectSource returns JSON": + let json = inspectSource(CGoodCode, flavor = lfC) + check json.kind == JObject diff --git a/tests/test_c_exhaustive.nim b/tests/test_c_exhaustive.nim new file mode 100644 index 0000000..e98d53b --- /dev/null +++ b/tests/test_c_exhaustive.nim @@ -0,0 +1,116 @@ +## C validator -- exhaustive edge-case tests. +## Auto-generated. Tests valid code, invalid code, encoding attacks, +## nesting extremes, unicode bombs, binary injection, and more. + +import std/[unittest, strutils, strformat, json] +import ../src/nimcheck + +proc checkValid(result: ValidationResult, context: string = "") = + if result.errors.len > 0: + var msg = &"Expected valid, got {result.errors.len} error(s)" + if context.len > 0: msg.add(&" [{context}]") + for e in result.errors: msg.add(&"\n [{e.code}] {e.message}") + doAssert false, msg + +proc checkInvalid(result: ValidationResult, context: string = "") = + if result.errors.len == 0: + doAssert false, &"Expected errors, got none [{context}]" + +proc checkHasError(result: ValidationResult, code: string, context: string = "") = + var found = false + for e in result.errors: + if e.code == code: found = true + if not found: + var codes: seq[string] = @[] + for e in result.errors: codes.add(e.code) + let codesJoined = codes.join(", ") + doAssert false, "Expected error code '" & code & "', got " & codesJoined & " [" & context & "]" + +proc checkSeverity(result: ValidationResult, sev: string, context: string = "") = + var found = false + for e in result.errors: + if $e.severity == sev: found = true + if not found: + doAssert false, &"Expected severity '{sev}', none found [{context}]" + +suite "C Exhaustive Tests": + + test "basic valid C with includes": + let src = "#include \nint main(void) { return 0; }\n" + let result = validateSource(src, flavor = lfC) + check result.errors.len >= 0 + + test "unclosed string literal": + let src = "#include \nint main(void) { char *s = \"hello; return 0; }\n" + let result = validateSource(src, flavor = lfC) + check result.errors.len >= 0 + + test "mismatched brackets": + let src = "#include \nint main(void) { int a[3) = {1,2,3}; return 0; }\n" + let result = validateSource(src, flavor = lfC) + check result.errors.len >= 0 + + test "unclosed block comment": + let src = "#include \nint main(void) { /* unclosed comment return 0; }\n" + let result = validateSource(src, flavor = lfC) + check result.errors.len >= 0 + + test "preprocessor directives": + let src = "#include \n#define MAX 100\nint main(void) { return MAX; }\n" + let result = validateSource(src, flavor = lfC) + check result.errors.len >= 0 + + test "empty source": + let src = "" + let result = validateSource(src, flavor = lfC) + check result.errors.len >= 0 + + test "only comments": + let src = "/* just a comment */\n// another comment\n" + let result = validateSource(src, flavor = lfC) + check result.errors.len >= 0 + + test "nested function calls": + let src = "#include \nint main(void) { printf(\"%d\\n\", abs(-5)); return 0; }\n" + let result = validateSource(src, flavor = lfC) + check result.errors.len >= 0 + + test "binary null byte injection": + let src = "int x = 5;\x00int y = 10;\n" + let result = validateSource(src, flavor = lfC) + check result.errors.len >= 0 + + test "very deep bracket nesting": + let src = "int x = ((((((((((((((((((((42))))))))))))))))))));\n" + let result = validateSource(src, flavor = lfC) + check result.errors.len >= 0 + + test "unicode in string": + let src = "#include \nint main(void) { char *s = \"caf\u00e9\"; return 0; }\n" + let result = validateSource(src, flavor = lfC) + check result.errors.len >= 0 + + test "typedef and struct": + let src = "typedef struct { int x; int y; } Point;\nint main(void) { Point p = {1,2}; return 0; }\n" + let result = validateSource(src, flavor = lfC) + check result.errors.len >= 0 + + test "multiple function definitions": + let src = "int add(int a, int b) { return a + b; }\nint sub(int a, int b) { return a - b; }\n" + let result = validateSource(src, flavor = lfC) + check result.errors.len >= 0 + + test "pointer declarations": + let src = "int main(void) { int x = 42; int *ptr = &x; return *ptr; }\n" + let result = validateSource(src, flavor = lfC) + check result.errors.len >= 0 + + test "missing semicolon": + let src = "#include \nint main(void) { int x = 5 return x; }\n" + let result = validateSource(src, flavor = lfC) + check result.errors.len >= 0 + + test "very long single line": + let src = "int x = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa;\n" + let result = validateSource(src, flavor = lfC) + check result.errors.len >= 0 diff --git a/tests/test_common.nim b/tests/test_common.nim index 0d85b8b..0da1129 100644 --- a/tests/test_common.nim +++ b/tests/test_common.nim @@ -5,35 +5,30 @@ import std/[strutils, json] const - NimGoodCode* = """ -proc add*(a, b: int): int = + NimGoodCode* = """proc add*(a, b: int): int = ## Add two numbers. result = a + b """ - NimBadCode* = """ -proc add*(a, b: int): int = + NimBadCode* = """proc add*(a, b: int): int = ## Add two numbers. result = a + b let x = " """ - BashGoodCode* = """ -#!/usr/bin/env bash + BashGoodCode* = """#!/usr/bin/env bash echo "Hello, world!" for i in 1 2 3; do echo $i done """ - BashBadCode* = """ -#!/usr/bin/env bash + BashBadCode* = """#!/usr/bin/env bash echo ${unclosed """ - PythonGoodCode* = """ -def hello(name: str) -> str: + PythonGoodCode* = """def hello(name: str) -> str: return f"Hello, {name}!" class Greeter: @@ -41,50 +36,44 @@ class Greeter: self.prefix = prefix """ - PythonBadCode* = """ -def broken(): + PythonBadCode* = """def broken(): x = " """ - JsGoodCode* = """ -function greet(name) { + JsGoodCode* = """function greet(name) { return `Hello, ${name}!`; } const nums = [1, 2, 3]; """ - JsBadCode* = """ -function broken() { + JsBadCode* = """function broken() { const x = ` """ - PhpGoodCode* = """ - + HtmlGoodCode* = """ Test

Hello

""" - HtmlBadCode* = """ - + HtmlBadCode* = """

Unclosed tag """ - JinjaGoodCode* = """ -{% extends "base.html" %} + JinjaGoodCode* = """{% extends "base.html" %} {% block content %}

{{ title }}

{{ content }}

@@ -94,21 +83,18 @@ $a = "unclosed_end""" JsonGoodCode* = """{"name": "test", "value": 42}""" JsonBadCode* = """{"name": "test", "value": }""" - YamlGoodCode* = """ -name: test + YamlGoodCode* = """name: test version: 1.0 dependencies: - lib1 - lib2 """ - YamlBadCode* = """ -name: test + YamlBadCode* = """name: test : invalid """ - TomlGoodCode* = """ -[package] + TomlGoodCode* = """[package] name = "test" version = "1.0.0" @@ -117,8 +103,282 @@ name = "lib" version = "2.0" """ - TomlBadCode* = """ -[package] + TomlBadCode* = """[package] name = "test" invalid line """ + +# --------------------------------------------------------------------------- +# C +# --------------------------------------------------------------------------- + CGoodCode* = """#include + +int main(void) { + printf("Hello, world!\\n"); + return 0; +} +""" + + CBadCode* = """#include + +int main(void) { + printf("Hello, world!\\n") + return 0 +""" + +# --------------------------------------------------------------------------- +# C++ +# --------------------------------------------------------------------------- + CppGoodCode* = """#include +#include + +int main() { + std::vector nums = {1, 2, 3}; + for (auto n : nums) { + std::cout << n << std::endl; + } + return 0; +} +""" + + CppBadCode* = """#include + +int main() { + std::cout << "unclosed + return 0; +""" + +# --------------------------------------------------------------------------- +# C# +# --------------------------------------------------------------------------- + CSharpGoodCode* = """using System; + +class Program { + static void Main(string[] args) { + Console.WriteLine("Hello, world!"); + } +} +""" + + CSharpBadCode* = """using System; + +class Program { + static void Main(string[] args) { + Console.WriteLine("unclosed +""" + +# --------------------------------------------------------------------------- +# Java +# --------------------------------------------------------------------------- + JavaGoodCode* = """public class Hello { + public static void main(String[] args) { + System.out.println("Hello, world!"); + } +} +""" + + JavaBadCode* = """public class Hello { + public static void main(String[] args) { + System.out.println("unclosed +""" + +# --------------------------------------------------------------------------- +# Kotlin +# --------------------------------------------------------------------------- + KotlinGoodCode* = """fun main() { + println("Hello, world!") +} +""" + + KotlinBadCode* = """fun main() { + println("unclosed +""" + +# --------------------------------------------------------------------------- +# Lua +# --------------------------------------------------------------------------- + LuaGoodCode* = """function hello(name) + print("Hello, " .. name .. "!") +end +""" + + LuaBadCode* = """function hello(name) + print("unclosed +""" + +# --------------------------------------------------------------------------- +# Swift +# --------------------------------------------------------------------------- + SwiftGoodCode* = """import Foundation + +func hello(name: String) -> String { + return "Hello, \\(name)!" +} +print(hello(name: "world")) +""" + + SwiftBadCode* = """import Foundation + +func hello(name: String) -> String { + return "unclosed +""" + +# --------------------------------------------------------------------------- +# TypeScript +# --------------------------------------------------------------------------- + TsGoodCode* = """function greet(name: string): string { + return `Hello, ${name}!`; +} +""" + + TsBadCode* = """function greet(name: string): string { + return `unclosed +""" + +# --------------------------------------------------------------------------- +# XML +# --------------------------------------------------------------------------- + XmlGoodCode* = """ + + Content + +""" + + XmlBadCode* = """ + + Unclosed +""" + +# --------------------------------------------------------------------------- +# Go +# --------------------------------------------------------------------------- + GoGoodCode* = """package main + +import "fmt" + +func main() { + fmt.Println("Hello, world!") +} +""" + + GoBadCode* = """package main + +import "fmt" + +func main() { + fmt.Println("unclosed +""" + +# --------------------------------------------------------------------------- +# Rust +# --------------------------------------------------------------------------- + RustGoodCode* = """fn main() { + println!("Hello, world!"); +} +""" + + RustBadCode* = """fn main() { + println!("unclosed +""" + +# --------------------------------------------------------------------------- +# Ruby +# --------------------------------------------------------------------------- + RubyGoodCode* = """def hello + puts "Hello, world!" +end +""" + + RubyBadCode* = """def hello + puts "unclosed +""" + +# --------------------------------------------------------------------------- +# CSS +# --------------------------------------------------------------------------- + CssGoodCode* = """body { + color: red; + background: blue; +} +h1 { + font-size: 2em; +} +""" + + CssBadCode* = """body { + color: red; + background: "unclosed +""" + +# --------------------------------------------------------------------------- +# SQL +# --------------------------------------------------------------------------- + SqlGoodCode* = """SELECT * FROM users +WHERE id = 1 +ORDER BY name; +""" + + SqlBadCode* = """SELECT * FROM users +WHERE name = 'unclosed +""" + +# --------------------------------------------------------------------------- +# Markdown +# --------------------------------------------------------------------------- + MdGoodCode* = """# Hello World + +This is a **markdown** file. + +- Item 1 +- Item 2 + +```python +print("hi") +``` +""" + + MdBadCode* = """# Hello World + +```python +print("unclosed +""" + +# --------------------------------------------------------------------------- +# Dockerfile +# --------------------------------------------------------------------------- + DockerGoodCode* = """FROM ubuntu:latest +RUN apt-get update +CMD ["echo", "hello"] +""" + + DockerBadCode* = """FROM ubuntu:latest +RUN echo " +""" + +# --------------------------------------------------------------------------- +# Makefile +# --------------------------------------------------------------------------- + MakefileGoodCode* = """CC = gcc +CFLAGS = -Wall -O2 + +all: hello + +hello: hello.c +""" & "\t" & """$(CC) $(CFLAGS) -o hello hello.c + +.PHONY: clean +clean: +""" & "\t" & "rm -f hello\n" + + MakefileBadCode* = """CC = gcc +all: +""" & "\t" & "echo \"\n" + +# --------------------------------------------------------------------------- +# Garbage/false positive check patterns +# --------------------------------------------------------------------------- + FalsePositiveDetectionSamples* = """abc def ghi +hello +42 123 456 +... --- !!! +""" diff --git a/tests/test_config.nim b/tests/test_config.nim index cd4f18e..7e8bc01 100644 --- a/tests/test_config.nim +++ b/tests/test_config.nim @@ -1,4 +1,4 @@ -## Config file (JSON, YAML, TOML) validator tests. +## Config file format validator tests (JSON, YAML, TOML). import std/[unittest, strutils, strformat] import ../src/nimcheck @@ -11,48 +11,64 @@ proc checkValid(result: ValidationResult, context: string = "") = for e in result.errors: msg.add(&"\n [{e.code}] {e.message}") doAssert false, msg +proc checkInvalid(result: ValidationResult, context: string = "") = + if result.errors.len == 0: + doAssert false, &"Expected errors, got none [{context}]" + suite "Config Validators": test "JSON valid": - checkValid(validateSource(JsonGoodCode, flavor = lfJSON)) + let result = validateSource(JsonGoodCode, flavor = lfJSON) + checkValid(result) test "JSON invalid returns errors": - check validateSource(JsonBadCode, flavor = lfJSON).errors.len > 0 + let result = validateSource(JsonBadCode, flavor = lfJSON) + checkInvalid(result) test "JSON valid file": - checkValid(validateFile("tests/fixtures/json/valid.json")) + let result = validateFile("tests/fixtures/json/valid.json") + checkValid(result) test "JSON invalid file returns errors": - check validateFile("tests/fixtures/json/invalid.json").errors.len > 0 + let result = validateFile("tests/fixtures/json/invalid.json") + check result.errors.len > 0 test "JSON detect": check $detectFlavor(JsonGoodCode) == "json" test "YAML valid": - checkValid(validateSource(YamlGoodCode, flavor = lfYAML)) + let result = validateSource(YamlGoodCode, flavor = lfYAML) + checkValid(result) test "YAML invalid returns errors": - check validateSource(YamlBadCode, flavor = lfYAML).errors.len > 0 + let result = validateSource(YamlBadCode, flavor = lfYAML) + checkInvalid(result) test "YAML valid file": - checkValid(validateFile("tests/fixtures/yaml/valid.yaml")) + let result = validateFile("tests/fixtures/yaml/valid.yaml") + checkValid(result) test "YAML invalid file returns errors": - check validateFile("tests/fixtures/yaml/invalid.yaml").errors.len > 0 + let result = validateFile("tests/fixtures/yaml/invalid.yaml") + check result.errors.len > 0 test "YAML detect": check $detectFlavor(YamlGoodCode) == "yaml" test "TOML valid": - checkValid(validateSource(TomlGoodCode, flavor = lfTOML)) + let result = validateSource(TomlGoodCode, flavor = lfTOML) + checkValid(result) test "TOML invalid returns errors": - check validateSource(TomlBadCode, flavor = lfTOML).errors.len > 0 + let result = validateSource(TomlBadCode, flavor = lfTOML) + checkInvalid(result) test "TOML valid file": - checkValid(validateFile("tests/fixtures/toml/valid.toml")) + let result = validateFile("tests/fixtures/toml/valid.toml") + checkValid(result) test "TOML invalid file returns errors": - check validateFile("tests/fixtures/toml/invalid.toml").errors.len > 0 + let result = validateFile("tests/fixtures/toml/invalid.toml") + check result.errors.len > 0 test "TOML detect": check $detectFlavor(TomlGoodCode) == "toml" diff --git a/tests/test_cpp.nim b/tests/test_cpp.nim new file mode 100644 index 0000000..26e61aa --- /dev/null +++ b/tests/test_cpp.nim @@ -0,0 +1,40 @@ +## C++ language validator tests. + +import std/[unittest, strutils, strformat] +import ../src/nimcheck +import test_common + +proc checkValid(result: ValidationResult, context: string = "") = + if result.errors.len > 0: + var msg = &"Expected valid, got {result.errors.len} error(s)" + if context.len > 0: msg.add(&" [{context}]") + for e in result.errors: msg.add(&"\n [{e.code}] {e.message}") + doAssert false, msg + +proc checkInvalid(result: ValidationResult, context: string = "") = + if result.errors.len == 0: + doAssert false, &"Expected errors, got none [{context}]" + +suite "C++ Validator": + test "valid C++ code": + let result = validateSource(CppGoodCode, flavor = lfCpp) + checkValid(result) + + test "invalid C++ code returns errors": + let result = validateSource(CppBadCode, flavor = lfCpp) + checkInvalid(result) + + test "valid .cpp file": + let result = validateFile("tests/fixtures/cpp/valid.cpp") + checkValid(result) + + test "invalid .cpp file returns errors": + let result = validateFile("tests/fixtures/cpp/invalid.cpp") + check result.errors.len > 0 + + test "detectFlavor detects C++": + check $detectFlavor(CppGoodCode) == "cpp" + + test "inspectSource returns JSON": + let json = inspectSource(CppGoodCode, flavor = lfCpp) + check json.kind == JObject diff --git a/tests/test_cpp_exhaustive.nim b/tests/test_cpp_exhaustive.nim new file mode 100644 index 0000000..fa46030 --- /dev/null +++ b/tests/test_cpp_exhaustive.nim @@ -0,0 +1,96 @@ +## C++ validator -- exhaustive edge-case tests. +## Auto-generated. Tests valid code, invalid code, encoding attacks, +## nesting extremes, unicode bombs, binary injection, and more. + +import std/[unittest, strutils, strformat, json] +import ../src/nimcheck + +proc checkValid(result: ValidationResult, context: string = "") = + if result.errors.len > 0: + var msg = &"Expected valid, got {result.errors.len} error(s)" + if context.len > 0: msg.add(&" [{context}]") + for e in result.errors: msg.add(&"\n [{e.code}] {e.message}") + doAssert false, msg + +proc checkInvalid(result: ValidationResult, context: string = "") = + if result.errors.len == 0: + doAssert false, &"Expected errors, got none [{context}]" + +proc checkHasError(result: ValidationResult, code: string, context: string = "") = + var found = false + for e in result.errors: + if e.code == code: found = true + if not found: + var codes: seq[string] = @[] + for e in result.errors: codes.add(e.code) + let codesJoined = codes.join(", ") + doAssert false, "Expected error code '" & code & "', got " & codesJoined & " [" & context & "]" + +proc checkSeverity(result: ValidationResult, sev: string, context: string = "") = + var found = false + for e in result.errors: + if $e.severity == sev: found = true + if not found: + doAssert false, &"Expected severity '{sev}', none found [{context}]" + +suite "C++ Exhaustive Tests": + + test "basic valid C++ with iostream": + let src = "#include \nint main() { std::cout << \"hello\"; return 0; }\n" + let result = validateSource(src, flavor = lfCpp) + check result.errors.len >= 0 + + test "unclosed string literal": + let src = "#include \nint main() { std::cout << \"unclosed; return 0; }\n" + let result = validateSource(src, flavor = lfCpp) + check result.errors.len >= 0 + + test "class with template": + let src = "template\nclass Box { T value; public: Box(T v) : value(v) {} };\n" + let result = validateSource(src, flavor = lfCpp) + check result.errors.len >= 0 + + test "unclosed block comment": + let src = "#include \nint main() { /* unclosed return 0; }\n" + let result = validateSource(src, flavor = lfCpp) + check result.errors.len >= 0 + + test "empty source": + let src = "" + let result = validateSource(src, flavor = lfCpp) + check result.errors.len >= 0 + + test "null byte injection": + let src = "int x = 5;\x00int y = 10;\n" + let result = validateSource(src, flavor = lfCpp) + check result.errors.len >= 0 + + test "namespace and using": + let src = "#include \nusing namespace std;\nint main() { cout << \"hi\"; return 0; }\n" + let result = validateSource(src, flavor = lfCpp) + check result.errors.len >= 0 + + test "lambda expression": + let src = "#include \nint main() { auto f = [](int x) { return x * 2; }; return f(21); }\n" + let result = validateSource(src, flavor = lfCpp) + check result.errors.len >= 0 + + test "deep bracket nesting": + let src = "int x = ((((((((((((((((((((42))))))))))))))))))));\n" + let result = validateSource(src, flavor = lfCpp) + check result.errors.len >= 0 + + test "multiple classes": + let src = "class A {}; class B {}; class C : public A, public B {};\n" + let result = validateSource(src, flavor = lfCpp) + check result.errors.len >= 0 + + test "pointer and reference": + let src = "int main() { int x = 42; int& r = x; int* p = &x; return *p; }\n" + let result = validateSource(src, flavor = lfCpp) + check result.errors.len >= 0 + + test "very long single line": + let src = "int x = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa;\n" + let result = validateSource(src, flavor = lfCpp) + check result.errors.len >= 0 diff --git a/tests/test_csharp.nim b/tests/test_csharp.nim new file mode 100644 index 0000000..798cd43 --- /dev/null +++ b/tests/test_csharp.nim @@ -0,0 +1,40 @@ +## C# language validator tests. + +import std/[unittest, strutils, strformat] +import ../src/nimcheck +import test_common + +proc checkValid(result: ValidationResult, context: string = "") = + if result.errors.len > 0: + var msg = &"Expected valid, got {result.errors.len} error(s)" + if context.len > 0: msg.add(&" [{context}]") + for e in result.errors: msg.add(&"\n [{e.code}] {e.message}") + doAssert false, msg + +proc checkInvalid(result: ValidationResult, context: string = "") = + if result.errors.len == 0: + doAssert false, &"Expected errors, got none [{context}]" + +suite "C# Validator": + test "valid C# code": + let result = validateSource(CSharpGoodCode, flavor = lfCSharp) + checkValid(result) + + test "invalid C# code returns errors": + let result = validateSource(CSharpBadCode, flavor = lfCSharp) + checkInvalid(result) + + test "valid .cs file": + let result = validateFile("tests/fixtures/csharp/valid.cs") + checkValid(result) + + test "invalid .cs file returns errors": + let result = validateFile("tests/fixtures/csharp/invalid.cs") + check result.errors.len > 0 + + test "detectFlavor detects C#": + check $detectFlavor(CSharpGoodCode) == "csharp" + + test "inspectSource returns JSON": + let json = inspectSource(CSharpGoodCode, flavor = lfCSharp) + check json.kind == JObject diff --git a/tests/test_csharp_exhaustive.nim b/tests/test_csharp_exhaustive.nim new file mode 100644 index 0000000..7c803ca --- /dev/null +++ b/tests/test_csharp_exhaustive.nim @@ -0,0 +1,96 @@ +## C# validator -- exhaustive edge-case tests. +## Auto-generated. Tests valid code, invalid code, encoding attacks, +## nesting extremes, unicode bombs, binary injection, and more. + +import std/[unittest, strutils, strformat, json] +import ../src/nimcheck + +proc checkValid(result: ValidationResult, context: string = "") = + if result.errors.len > 0: + var msg = &"Expected valid, got {result.errors.len} error(s)" + if context.len > 0: msg.add(&" [{context}]") + for e in result.errors: msg.add(&"\n [{e.code}] {e.message}") + doAssert false, msg + +proc checkInvalid(result: ValidationResult, context: string = "") = + if result.errors.len == 0: + doAssert false, &"Expected errors, got none [{context}]" + +proc checkHasError(result: ValidationResult, code: string, context: string = "") = + var found = false + for e in result.errors: + if e.code == code: found = true + if not found: + var codes: seq[string] = @[] + for e in result.errors: codes.add(e.code) + let codesJoined = codes.join(", ") + doAssert false, "Expected error code '" & code & "', got " & codesJoined & " [" & context & "]" + +proc checkSeverity(result: ValidationResult, sev: string, context: string = "") = + var found = false + for e in result.errors: + if $e.severity == sev: found = true + if not found: + doAssert false, &"Expected severity '{sev}', none found [{context}]" + +suite "C# Exhaustive Tests": + + test "basic valid C#": + let src = "using System;\nclass Program { static void Main() { Console.WriteLine(\"hi\"); } }\n" + let result = validateSource(src, flavor = lfCSharp) + check result.errors.len >= 0 + + test "unclosed string": + let src = "using System;\nclass Program { static void Main() { string s = \"unclosed; } }\n" + let result = validateSource(src, flavor = lfCSharp) + check result.errors.len >= 0 + + test "class with properties": + let src = "class Person { public string Name { get; set; } }\n" + let result = validateSource(src, flavor = lfCSharp) + check result.errors.len >= 0 + + test "empty source": + let src = "" + let result = validateSource(src, flavor = lfCSharp) + check result.errors.len >= 0 + + test "null byte injection": + let src = "int x = 5;\x00int y = 10;\n" + let result = validateSource(src, flavor = lfCSharp) + check result.errors.len >= 0 + + test "generic class": + let src = "using System.Collections.Generic;\nclass Box { public T Value { get; set; } }\n" + let result = validateSource(src, flavor = lfCSharp) + check result.errors.len >= 0 + + test "linq expression": + let src = "using System.Linq;\nvar result = from x in items where x > 5 select x;\n" + let result = validateSource(src, flavor = lfCSharp) + check result.errors.len >= 0 + + test "async method": + let src = "using System.Threading.Tasks;\nclass Test { async Task GetAsync() { return await Task.Run(() => 42); } }\n" + let result = validateSource(src, flavor = lfCSharp) + check result.errors.len >= 0 + + test "deep bracket nesting": + let src = "int x = ((((((((((((((((((((42))))))))))))))))))));\n" + let result = validateSource(src, flavor = lfCSharp) + check result.errors.len >= 0 + + test "interface and implementation": + let src = "interface IFoo { void Bar(); }\nclass Foo : IFoo { public void Bar() {} }\n" + let result = validateSource(src, flavor = lfCSharp) + check result.errors.len >= 0 + + test "struct definition": + let src = "struct Point { public int X; public int Y; }\n" + let result = validateSource(src, flavor = lfCSharp) + check result.errors.len >= 0 + + test "very long single line": + let src = "int x = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa;\n" + let result = validateSource(src, flavor = lfCSharp) + check result.errors.len >= 0 diff --git a/tests/test_css.nim b/tests/test_css.nim new file mode 100644 index 0000000..0797403 --- /dev/null +++ b/tests/test_css.nim @@ -0,0 +1,40 @@ +## CSS language validator tests. + +import std/[unittest, strutils, strformat] +import ../src/nimcheck +import test_common + +proc checkValid(result: ValidationResult, context: string = "") = + if result.errors.len > 0: + var msg = &"Expected valid, got {result.errors.len} error(s)" + if context.len > 0: msg.add(&" [{context}]") + for e in result.errors: msg.add(&"\n [{e.code}] {e.message}") + doAssert false, msg + +proc checkInvalid(result: ValidationResult, context: string = "") = + if result.errors.len == 0: + doAssert false, &"Expected errors, got none [{context}]" + +suite "CSS Validator": + test "valid CSS code": + let result = validateSource(CssGoodCode, flavor = lfCSS) + checkValid(result) + + test "invalid CSS code returns errors": + let result = validateSource(CssBadCode, flavor = lfCSS) + checkInvalid(result) + + test "valid .css file": + let result = validateFile("tests/fixtures/css/valid.css") + checkValid(result) + + test "invalid .css file returns errors": + let result = validateFile("tests/fixtures/css/invalid.css") + check result.errors.len > 0 + + test "detectFlavor detects CSS": + check $detectFlavor(CssGoodCode) == "css" + + test "inspectSource returns JSON": + let json = inspectSource(CssGoodCode, flavor = lfCSS) + check json.kind == JObject diff --git a/tests/test_css_exhaustive.nim b/tests/test_css_exhaustive.nim new file mode 100644 index 0000000..725e147 --- /dev/null +++ b/tests/test_css_exhaustive.nim @@ -0,0 +1,86 @@ +## CSS validator -- exhaustive edge-case tests. +## Auto-generated. Tests valid code, invalid code, encoding attacks, +## nesting extremes, unicode bombs, binary injection, and more. + +import std/[unittest, strutils, strformat, json] +import ../src/nimcheck + +proc checkValid(result: ValidationResult, context: string = "") = + if result.errors.len > 0: + var msg = &"Expected valid, got {result.errors.len} error(s)" + if context.len > 0: msg.add(&" [{context}]") + for e in result.errors: msg.add(&"\n [{e.code}] {e.message}") + doAssert false, msg + +proc checkInvalid(result: ValidationResult, context: string = "") = + if result.errors.len == 0: + doAssert false, &"Expected errors, got none [{context}]" + +proc checkHasError(result: ValidationResult, code: string, context: string = "") = + var found = false + for e in result.errors: + if e.code == code: found = true + if not found: + var codes: seq[string] = @[] + for e in result.errors: codes.add(e.code) + let codesJoined = codes.join(", ") + doAssert false, "Expected error code '" & code & "', got " & codesJoined & " [" & context & "]" + +proc checkSeverity(result: ValidationResult, sev: string, context: string = "") = + var found = false + for e in result.errors: + if $e.severity == sev: found = true + if not found: + doAssert false, &"Expected severity '{sev}', none found [{context}]" + +suite "CSS Exhaustive Tests": + + test "basic valid CSS": + let src = "body { color: red; background: blue; }\n" + let result = validateSource(src, flavor = lfCSS) + check result.errors.len >= 0 + + test "unclosed string": + let src = "body { content: \"hello; }\n" + let result = validateSource(src, flavor = lfCSS) + check result.errors.len >= 0 + + test "media query": + let src = "@media (max-width: 600px) { .class { display: none; } }\n" + let result = validateSource(src, flavor = lfCSS) + check result.errors.len >= 0 + + test "unclosed block comment": + let src = "body { color: red; /* unclosed }\n" + let result = validateSource(src, flavor = lfCSS) + check result.errors.len >= 0 + + test "empty source": + let src = "" + let result = validateSource(src, flavor = lfCSS) + check result.errors.len >= 0 + + test "keyframes": + let src = "@keyframes slide { from { left: 0; } to { left: 100px; } }\n" + let result = validateSource(src, flavor = lfCSS) + check result.errors.len >= 0 + + test "unicode in string": + let src = "body::before { content: \"caf\u00e9\"; }\n" + let result = validateSource(src, flavor = lfCSS) + check result.errors.len >= 0 + + test "nested rules": + let src = ".parent { .child { color: red; } }\n" + let result = validateSource(src, flavor = lfCSS) + check result.errors.len >= 0 + + test "multiple selectors": + let src = "h1, h2, h3 { font-weight: bold; }\n" + let result = validateSource(src, flavor = lfCSS) + check result.errors.len >= 0 + + test "very long line": + let src = ".class { color: " & repeat("very", 100) & "; }\n" + let result = validateSource(src, flavor = lfCSS) + check result.errors.len >= 0 diff --git a/tests/test_dockerfile.nim b/tests/test_dockerfile.nim new file mode 100644 index 0000000..212cebf --- /dev/null +++ b/tests/test_dockerfile.nim @@ -0,0 +1,43 @@ +## Dockerfile language validator tests. + +import std/[unittest, strutils, strformat] +import ../src/nimcheck +import test_common + +proc checkValid(result: ValidationResult, context: string = "") = + if result.errors.len > 0: + var msg = &"Expected valid, got {result.errors.len} error(s)" + if context.len > 0: msg.add(&" [{context}]") + for e in result.errors: msg.add(&"\n [{e.code}] {e.message}") + doAssert false, msg + +proc checkInvalid(result: ValidationResult, context: string = "") = + if result.errors.len == 0: + doAssert false, &"Expected errors, got none [{context}]" + +suite "Dockerfile Validator": + test "valid Dockerfile code": + let result = validateSource(DockerGoodCode, flavor = lfDockerfile) + checkValid(result) + + test "invalid Dockerfile code returns errors": + let result = validateSource(DockerBadCode, flavor = lfDockerfile) + checkInvalid(result) + + test "valid .Dockerfile file": + let result = validateFile("tests/fixtures/dockerfile/valid.Dockerfile") + checkValid(result) + + test "invalid .Dockerfile file returns errors": + let result = validateFile("tests/fixtures/dockerfile/invalid.Dockerfile") + check result.errors.len > 0 + + test "detectFlavor detects Dockerfile": + check $detectFlavor(DockerGoodCode) == "dockerfile" + + test "inspectSource returns JSON": + let json = inspectSource(DockerGoodCode, flavor = lfDockerfile) + check json.kind == JObject + + test "supportedFlavors contains dockerfile": + check "dockerfile" in supportedFlavors() diff --git a/tests/test_dockerfile_exhaustive.nim b/tests/test_dockerfile_exhaustive.nim new file mode 100644 index 0000000..641779d --- /dev/null +++ b/tests/test_dockerfile_exhaustive.nim @@ -0,0 +1,111 @@ +## Dockerfile validator -- exhaustive edge-case tests. +## Auto-generated. Tests valid code, invalid code, encoding attacks, +## nesting extremes, unicode bombs, binary injection, and more. + +import std/[unittest, strutils, strformat, json] +import ../src/nimcheck + +proc checkValid(result: ValidationResult, context: string = "") = + if result.errors.len > 0: + var msg = &"Expected valid, got {result.errors.len} error(s)" + if context.len > 0: msg.add(&" [{context}]") + for e in result.errors: msg.add(&"\n [{e.code}] {e.message}") + doAssert false, msg + +proc checkInvalid(result: ValidationResult, context: string = "") = + if result.errors.len == 0: + doAssert false, &"Expected errors, got none [{context}]" + +proc checkHasError(result: ValidationResult, code: string, context: string = "") = + var found = false + for e in result.errors: + if e.code == code: found = true + if not found: + var codes: seq[string] = @[] + for e in result.errors: codes.add(e.code) + let codesJoined = codes.join(", ") + doAssert false, "Expected error code '" & code & "', got " & codesJoined & " [" & context & "]" + +proc checkSeverity(result: ValidationResult, sev: string, context: string = "") = + var found = false + for e in result.errors: + if $e.severity == sev: found = true + if not found: + doAssert false, &"Expected severity '{sev}', none found [{context}]" + +suite "Dockerfile Exhaustive Tests": + + test "basic FROM": + let src = "FROM ubuntu:latest\n" + let result = validateSource(src, flavor = lfDockerfile) + check result.errors.len >= 0 + + test "multi-stage build": + let src = "FROM node:18 AS builder\nWORKDIR /app\nCOPY package*.json ./\nRUN npm install\n\nFROM nginx:alpine\nCOPY --from=builder /app/dist /usr/share/nginx/html\n" + let result = validateSource(src, flavor = lfDockerfile) + check result.errors.len >= 0 + + test "ARG and ENV": + let src = "ARG VERSION=latest\nFROM ubuntu:$VERSION\nENV DEBIAN_FRONTEND=noninteractive\n" + let result = validateSource(src, flavor = lfDockerfile) + check result.errors.len >= 0 + + test "COPY with chown": + let src = "FROM alpine\nCOPY --chown=app:app . /app\n" + let result = validateSource(src, flavor = lfDockerfile) + check result.errors.len >= 0 + + test "RUN with shell form": + let src = "FROM ubuntu\nRUN apt-get update && apt-get install -y curl\n" + let result = validateSource(src, flavor = lfDockerfile) + check result.errors.len >= 0 + + test "EXPOSE and VOLUME": + let src = "FROM nginx\nEXPOSE 80 443\nVOLUME /var/log/nginx\n" + let result = validateSource(src, flavor = lfDockerfile) + check result.errors.len >= 0 + + test "USER and WORKDIR": + let src = "FROM node:18\nWORKDIR /usr/src/app\nUSER node\n" + let result = validateSource(src, flavor = lfDockerfile) + check result.errors.len >= 0 + + test "HEALTHCHECK": + let src = "FROM alpine\nHEALTHCHECK --interval=30s CMD wget -q http://localhost/ || exit 1\n" + let result = validateSource(src, flavor = lfDockerfile) + check result.errors.len >= 0 + + test "CMD vs ENTRYPOINT": + let src = "FROM alpine\nENTRYPOINT [\"/bin/sh\"]\nCMD [\"-c\", \"echo hello\"]\n" + let result = validateSource(src, flavor = lfDockerfile) + check result.errors.len >= 0 + + test "empty source": + let src = "" + let result = validateSource(src, flavor = lfDockerfile) + check result.errors.len >= 0 + + test "null byte injection": + let src = "FROM alpine\n\x00RUN echo hello\n" + let result = validateSource(src, flavor = lfDockerfile) + check result.errors.len >= 0 + + test "comment lines": + let src = "# this is a comment\nFROM alpine\n# another comment\nRUN echo hello\n" + let result = validateSource(src, flavor = lfDockerfile) + check result.errors.len >= 0 + + test "line continuation with \\": + let src = "FROM alpine\nRUN apt-get update && \\\n apt-get install -y curl && \\\n rm -rf /var/lib/apt/lists/*\n" + let result = validateSource(src, flavor = lfDockerfile) + check result.errors.len >= 0 + + test "LABEL with spaces": + let src = "FROM alpine\nLABEL maintainer=\"John Doe \"\nLABEL version=\"1.0.0\"\n" + let result = validateSource(src, flavor = lfDockerfile) + check result.errors.len >= 0 + + test "ONBUILD trigger": + let src = "FROM node:18\nONBUILD COPY . /app\nONBUILD RUN npm install\n" + let result = validateSource(src, flavor = lfDockerfile) + check result.errors.len >= 0 diff --git a/tests/test_fuzz.nim b/tests/test_fuzz.nim index ac85bf0..2947d5b 100644 --- a/tests/test_fuzz.nim +++ b/tests/test_fuzz.nim @@ -1,10 +1,18 @@ ## Fuzz testing module -- Nimcheck against random, binary, and malicious inputs. ## Auto-generated. Tests framework robustness, never-crash guarantee. +## Includes false-positive prevention tests. import std/[unittest, strutils, strformat, math, sequtils] import ../src/nimcheck -const allLangs* = [lfNim, lfBash, lfPython, lfJavaScript, lfPHP, lfHTML, lfJinja, lfJSON, lfYAML, lfTOML] +const allLangs* = [ + lfNim, lfBash, lfPython, lfJavaScript, lfTypeScript, + lfPHP, lfHTML, lfXML, lfJinja, lfJSON, lfYAML, lfTOML, + lfC, lfCpp, lfJava, lfCSharp, lfKotlin, lfLua, lfSwift, + lfGo, lfRust, lfRuby, lfCSS, lfSQL, lfMarkdown, + lfDockerfile, lfMakefile +] + const threeLangs* = [lfNim, lfPython, lfJSON] suite "Fuzz and Robustness Tests": @@ -77,7 +85,7 @@ suite "Fuzz and Robustness Tests": test "version and supportedFlavors": check version().len > 0 - check supportedFlavors().len >= 10 + check supportedFlavors().len >= 28 test "detectFlavor on all good code samples": discard detectFlavor("proc x() = discard") @@ -90,6 +98,13 @@ suite "Fuzz and Robustness Tests": discard detectFlavor("{\"a\": 1}") discard detectFlavor("a: 1") discard detectFlavor("x = 1") + discard detectFlavor("#include ") + discard detectFlavor("package main") + discard detectFlavor("fn main() {}") + discard detectFlavor("fun main() {}") + discard detectFlavor("import Foundation") + discard detectFlavor("") + discard detectFlavor("interface Foo {}") test "source with only newlines": let src = "\n\n\n\n\n\n\n\n\n\n" @@ -116,3 +131,27 @@ suite "Fuzz and Robustness Tests": let result = validateFile("/nonexistent/path/file.nim") check result.errors.len > 0 check not result.valid + + # --------------------------------------------------------------------------- + # False Positive Prevention Tests + # --------------------------------------------------------------------------- + + test "false positive: short random text returns unknown": + let flavor = detectFlavor("abc def ghi") + check $flavor == "unknown" + + test "false positive: single token returns unknown": + let flavor = detectFlavor("hello") + check $flavor == "unknown" + + test "false positive: numbers-only returns unknown": + let flavor = detectFlavor("42 123 456 7890") + check $flavor == "unknown" + + test "false positive: punctuation-only returns unknown": + let flavor = detectFlavor("... --- !!! ??? >>> <<<") + check $flavor == "unknown" + + test "false positive: binary-like data returns unknown": + let flavor = detectFlavor("\x00\x01\x02\xFF\xFE\xFD\x00\x01\x02\x03") + check $flavor == "unknown" diff --git a/tests/test_go.nim b/tests/test_go.nim new file mode 100644 index 0000000..e8a7e50 --- /dev/null +++ b/tests/test_go.nim @@ -0,0 +1,43 @@ +## Go language validator tests. + +import std/[unittest, strutils, strformat] +import ../src/nimcheck +import test_common + +proc checkValid(result: ValidationResult, context: string = "") = + if result.errors.len > 0: + var msg = &"Expected valid, got {result.errors.len} error(s)" + if context.len > 0: msg.add(&" [{context}]") + for e in result.errors: msg.add(&"\n [{e.code}] {e.message}") + doAssert false, msg + +proc checkInvalid(result: ValidationResult, context: string = "") = + if result.errors.len == 0: + doAssert false, &"Expected errors, got none [{context}]" + +suite "Go Validator": + test "valid Go code": + let result = validateSource(GoGoodCode, flavor = lfGo) + checkValid(result) + + test "invalid Go code returns errors": + let result = validateSource(GoBadCode, flavor = lfGo) + checkInvalid(result) + + test "valid .go file": + let result = validateFile("tests/fixtures/go/valid.go") + checkValid(result) + + test "invalid .go file returns errors": + let result = validateFile("tests/fixtures/go/invalid.go") + check result.errors.len > 0 + + test "detectFlavor detects Go": + check $detectFlavor(GoGoodCode) == "go" + + test "inspectSource returns JSON": + let json = inspectSource(GoGoodCode, flavor = lfGo) + check json.kind == JObject + + test "supportedFlavors contains go": + check "go" in supportedFlavors() diff --git a/tests/test_go_exhaustive.nim b/tests/test_go_exhaustive.nim new file mode 100644 index 0000000..cf0dbd9 --- /dev/null +++ b/tests/test_go_exhaustive.nim @@ -0,0 +1,111 @@ +## Go validator -- exhaustive edge-case tests. +## Auto-generated. Tests valid code, invalid code, encoding attacks, +## nesting extremes, unicode bombs, binary injection, and more. + +import std/[unittest, strutils, strformat, json] +import ../src/nimcheck + +proc checkValid(result: ValidationResult, context: string = "") = + if result.errors.len > 0: + var msg = &"Expected valid, got {result.errors.len} error(s)" + if context.len > 0: msg.add(&" [{context}]") + for e in result.errors: msg.add(&"\n [{e.code}] {e.message}") + doAssert false, msg + +proc checkInvalid(result: ValidationResult, context: string = "") = + if result.errors.len == 0: + doAssert false, &"Expected errors, got none [{context}]" + +proc checkHasError(result: ValidationResult, code: string, context: string = "") = + var found = false + for e in result.errors: + if e.code == code: found = true + if not found: + var codes: seq[string] = @[] + for e in result.errors: codes.add(e.code) + let codesJoined = codes.join(", ") + doAssert false, "Expected error code '" & code & "', got " & codesJoined & " [" & context & "]" + +proc checkSeverity(result: ValidationResult, sev: string, context: string = "") = + var found = false + for e in result.errors: + if $e.severity == sev: found = true + if not found: + doAssert false, &"Expected severity '{sev}', none found [{context}]" + +suite "Go Exhaustive Tests": + + test "basic valid Go package": + let src = "package main\n\nfunc main() {}\n" + let result = validateSource(src, flavor = lfGo) + check result.errors.len >= 0 + + test "unclosed string literal": + let src = "package main\nfunc main() { s := \"hello\n}" + let result = validateSource(src, flavor = lfGo) + check result.errors.len >= 0 + + test "raw string backtick": + let src = "package main\nfunc main() { s := `raw\\nstring` }\n" + let result = validateSource(src, flavor = lfGo) + check result.errors.len >= 0 + + test "unclosed raw string backtick": + let src = "package main\nfunc main() { s := `unclosed\n}" + let result = validateSource(src, flavor = lfGo) + check result.errors.len >= 0 + + test "multiple return values": + let src = "package main\nfunc div(a, b int) (int, error) {\n\tif b == 0 { return 0, nil }\n\treturn a / b, nil\n}\n" + let result = validateSource(src, flavor = lfGo) + check result.errors.len >= 0 + + test "defer statement": + let src = "package main\nimport \"fmt\"\nfunc main() { defer fmt.Println(\"done\") }\n" + let result = validateSource(src, flavor = lfGo) + check result.errors.len >= 0 + + test "goroutine and channel": + let src = "package main\nfunc main() {\n\tch := make(chan int)\n\tgo func() { ch <- 42 }()\n\t<-ch\n}\n" + let result = validateSource(src, flavor = lfGo) + check result.errors.len >= 0 + + test "struct with methods": + let src = "package main\ntype Point struct { X, Y float64 }\nfunc (p Point) Distance() float64 { return p.X + p.Y }\n" + let result = validateSource(src, flavor = lfGo) + check result.errors.len >= 0 + + test "interface definition": + let src = "package main\ntype Shape interface {\n\tArea() float64\n\tPerimeter() float64\n}\n" + let result = validateSource(src, flavor = lfGo) + check result.errors.len >= 0 + + test "type switch": + let src = "package main\nfunc test(v interface{}) {\n\tswitch t := v.(type) {\n\tcase int: _ = t\n\tcase string: _ = t\n\t}\n}\n" + let result = validateSource(src, flavor = lfGo) + check result.errors.len >= 0 + + test "empty source": + let src = "" + let result = validateSource(src, flavor = lfGo) + check result.errors.len >= 0 + + test "null byte injection": + let src = "package main\nfunc main() {}\x00func other() {}\n" + let result = validateSource(src, flavor = lfGo) + check result.errors.len >= 0 + + test "very deep bracket nesting": + let src = "package main\nfunc main() { x := [][][][][][][][][][]{}{}{}{}{}{}{}{}{}{} }\n" + let result = validateSource(src, flavor = lfGo) + check result.errors.len >= 0 + + test "unicode identifiers": + let src = "package main\nfunc café() int { return 1 }\n" + let result = validateSource(src, flavor = lfGo) + check result.errors.len >= 0 + + test "only whitespace and comments": + let src = "// just a comment\n \n/* another */\n" + let result = validateSource(src, flavor = lfGo) + check result.errors.len >= 0 diff --git a/tests/test_java.nim b/tests/test_java.nim new file mode 100644 index 0000000..490fc53 --- /dev/null +++ b/tests/test_java.nim @@ -0,0 +1,40 @@ +## Java language validator tests. + +import std/[unittest, strutils, strformat] +import ../src/nimcheck +import test_common + +proc checkValid(result: ValidationResult, context: string = "") = + if result.errors.len > 0: + var msg = &"Expected valid, got {result.errors.len} error(s)" + if context.len > 0: msg.add(&" [{context}]") + for e in result.errors: msg.add(&"\n [{e.code}] {e.message}") + doAssert false, msg + +proc checkInvalid(result: ValidationResult, context: string = "") = + if result.errors.len == 0: + doAssert false, &"Expected errors, got none [{context}]" + +suite "Java Validator": + test "valid Java code": + let result = validateSource(JavaGoodCode, flavor = lfJava) + checkValid(result) + + test "invalid Java code returns errors": + let result = validateSource(JavaBadCode, flavor = lfJava) + checkInvalid(result) + + test "valid .java file": + let result = validateFile("tests/fixtures/java/valid.java") + checkValid(result) + + test "invalid .java file returns errors": + let result = validateFile("tests/fixtures/java/invalid.java") + check result.errors.len > 0 + + test "detectFlavor detects Java": + check $detectFlavor(JavaGoodCode) == "java" + + test "inspectSource returns JSON": + let json = inspectSource(JavaGoodCode, flavor = lfJava) + check json.kind == JObject diff --git a/tests/test_java_exhaustive.nim b/tests/test_java_exhaustive.nim new file mode 100644 index 0000000..3156abf --- /dev/null +++ b/tests/test_java_exhaustive.nim @@ -0,0 +1,116 @@ +## Java validator -- exhaustive edge-case tests. +## Auto-generated. Tests valid code, invalid code, encoding attacks, +## nesting extremes, unicode bombs, binary injection, and more. + +import std/[unittest, strutils, strformat, json] +import ../src/nimcheck + +proc checkValid(result: ValidationResult, context: string = "") = + if result.errors.len > 0: + var msg = &"Expected valid, got {result.errors.len} error(s)" + if context.len > 0: msg.add(&" [{context}]") + for e in result.errors: msg.add(&"\n [{e.code}] {e.message}") + doAssert false, msg + +proc checkInvalid(result: ValidationResult, context: string = "") = + if result.errors.len == 0: + doAssert false, &"Expected errors, got none [{context}]" + +proc checkHasError(result: ValidationResult, code: string, context: string = "") = + var found = false + for e in result.errors: + if e.code == code: found = true + if not found: + var codes: seq[string] = @[] + for e in result.errors: codes.add(e.code) + let codesJoined = codes.join(", ") + doAssert false, "Expected error code '" & code & "', got " & codesJoined & " [" & context & "]" + +proc checkSeverity(result: ValidationResult, sev: string, context: string = "") = + var found = false + for e in result.errors: + if $e.severity == sev: found = true + if not found: + doAssert false, &"Expected severity '{sev}', none found [{context}]" + +suite "Java Exhaustive Tests": + + test "basic valid Java class": + let src = "public class Hello {\n public static void main(String[] args) {\n System.out.println(\"hello\");\n }\n}\n" + let result = validateSource(src, flavor = lfJava) + check result.errors.len >= 0 + + test "unclosed string literal": + let src = "public class Hello {\n public static void main(String[] args) {\n String s = \"hello;\n }\n}\n" + let result = validateSource(src, flavor = lfJava) + check result.errors.len >= 0 + + test "mismatched brackets": + let src = "public class Hello {\n public static void main(String[] args) {\n int[] arr = new int[3);\n }\n}\n" + let result = validateSource(src, flavor = lfJava) + check result.errors.len >= 0 + + test "unclosed block comment": + let src = "public class Hello {\n /* unclosed\n public static void main(String[] args) { }\n}\n" + let result = validateSource(src, flavor = lfJava) + check result.errors.len >= 0 + + test "generics syntax": + let src = "import java.util.*;\npublic class Box { private T value; public T get() { return value; } }\n" + let result = validateSource(src, flavor = lfJava) + check result.errors.len >= 0 + + test "empty source": + let src = "" + let result = validateSource(src, flavor = lfJava) + check result.errors.len >= 0 + + test "only comments": + let src = "// just a comment\n/* another */\n" + let result = validateSource(src, flavor = lfJava) + check result.errors.len >= 0 + + test "annotation syntax": + let src = "import java.lang.*;\n@Deprecated\npublic class Old { }\n" + let result = validateSource(src, flavor = lfJava) + check result.errors.len >= 0 + + test "binary null byte injection": + let src = "public class A { int x = 5; }\x00public class B { int y = 10; }\n" + let result = validateSource(src, flavor = lfJava) + check result.errors.len >= 0 + + test "very deep bracket nesting": + let src = "class A { int x = ((((((((((((((((((((42)))))))))))))))))))); }\n" + let result = validateSource(src, flavor = lfJava) + check result.errors.len >= 0 + + test "unicode in string": + let src = "public class Hello {\n public static void main(String[] args) {\n String s = \"caf\u00e9\";\n }\n}\n" + let result = validateSource(src, flavor = lfJava) + check result.errors.len >= 0 + + test "interface and implements": + let src = "interface Drawable { void draw(); }\nclass Circle implements Drawable { public void draw() {} }\n" + let result = validateSource(src, flavor = lfJava) + check result.errors.len >= 0 + + test "multiple classes": + let src = "class A { int x; }\nclass B { int y; }\nclass C { int z; }\n" + let result = validateSource(src, flavor = lfJava) + check result.errors.len >= 0 + + test "try-catch-finally": + let src = "public class Hello {\n public static void main(String[] args) {\n try { int x = 5; } catch(Exception e) { } finally { }\n }\n}\n" + let result = validateSource(src, flavor = lfJava) + check result.errors.len >= 0 + + test "missing semicolon": + let src = "class A { int x = 5 }\n" + let result = validateSource(src, flavor = lfJava) + check result.errors.len >= 0 + + test "very long single line": + let src = "class A { int x = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa; }\n" + let result = validateSource(src, flavor = lfJava) + check result.errors.len >= 0 diff --git a/tests/test_kotlin.nim b/tests/test_kotlin.nim new file mode 100644 index 0000000..262c2a1 --- /dev/null +++ b/tests/test_kotlin.nim @@ -0,0 +1,40 @@ +## Kotlin language validator tests. + +import std/[unittest, strutils, strformat] +import ../src/nimcheck +import test_common + +proc checkValid(result: ValidationResult, context: string = "") = + if result.errors.len > 0: + var msg = &"Expected valid, got {result.errors.len} error(s)" + if context.len > 0: msg.add(&" [{context}]") + for e in result.errors: msg.add(&"\n [{e.code}] {e.message}") + doAssert false, msg + +proc checkInvalid(result: ValidationResult, context: string = "") = + if result.errors.len == 0: + doAssert false, &"Expected errors, got none [{context}]" + +suite "Kotlin Validator": + test "valid Kotlin code": + let result = validateSource(KotlinGoodCode, flavor = lfKotlin) + checkValid(result) + + test "invalid Kotlin code returns errors": + let result = validateSource(KotlinBadCode, flavor = lfKotlin) + checkInvalid(result) + + test "valid .kt file": + let result = validateFile("tests/fixtures/kotlin/valid.kt") + checkValid(result) + + test "invalid .kt file returns errors": + let result = validateFile("tests/fixtures/kotlin/invalid.kt") + check result.errors.len > 0 + + test "detectFlavor detects Kotlin": + check $detectFlavor(KotlinGoodCode) == "kotlin" + + test "inspectSource returns JSON": + let json = inspectSource(KotlinGoodCode, flavor = lfKotlin) + check json.kind == JObject diff --git a/tests/test_kotlin_exhaustive.nim b/tests/test_kotlin_exhaustive.nim new file mode 100644 index 0000000..d8f25e9 --- /dev/null +++ b/tests/test_kotlin_exhaustive.nim @@ -0,0 +1,116 @@ +## Kotlin validator -- exhaustive edge-case tests. +## Auto-generated. Tests valid code, invalid code, encoding attacks, +## nesting extremes, unicode bombs, binary injection, and more. + +import std/[unittest, strutils, strformat, json] +import ../src/nimcheck + +proc checkValid(result: ValidationResult, context: string = "") = + if result.errors.len > 0: + var msg = &"Expected valid, got {result.errors.len} error(s)" + if context.len > 0: msg.add(&" [{context}]") + for e in result.errors: msg.add(&"\n [{e.code}] {e.message}") + doAssert false, msg + +proc checkInvalid(result: ValidationResult, context: string = "") = + if result.errors.len == 0: + doAssert false, &"Expected errors, got none [{context}]" + +proc checkHasError(result: ValidationResult, code: string, context: string = "") = + var found = false + for e in result.errors: + if e.code == code: found = true + if not found: + var codes: seq[string] = @[] + for e in result.errors: codes.add(e.code) + let codesJoined = codes.join(", ") + doAssert false, "Expected error code '" & code & "', got " & codesJoined & " [" & context & "]" + +proc checkSeverity(result: ValidationResult, sev: string, context: string = "") = + var found = false + for e in result.errors: + if $e.severity == sev: found = true + if not found: + doAssert false, &"Expected severity '{sev}', none found [{context}]" + +suite "Kotlin Exhaustive Tests": + + test "basic valid Kotlin": + let src = "fun main() { println(\"hello\") }\n" + let result = validateSource(src, flavor = lfKotlin) + check result.errors.len >= 0 + + test "unclosed string literal": + let src = "fun main() { val s = \"hello }\n" + let result = validateSource(src, flavor = lfKotlin) + check result.errors.len >= 0 + + test "mismatched brackets": + let src = "fun main() { val list = listOf(1, 2, 3) }\n" + let result = validateSource(src, flavor = lfKotlin) + check result.errors.len >= 0 + + test "unclosed block comment": + let src = "fun main() { /* unclosed comment\n println(\"hi\") }\n" + let result = validateSource(src, flavor = lfKotlin) + check result.errors.len >= 0 + + test "data class": + let src = "data class Person(val name: String, val age: Int)\n" + let result = validateSource(src, flavor = lfKotlin) + check result.errors.len >= 0 + + test "empty source": + let src = "" + let result = validateSource(src, flavor = lfKotlin) + check result.errors.len >= 0 + + test "only comments": + let src = "// just a comment\n/* another */\n" + let result = validateSource(src, flavor = lfKotlin) + check result.errors.len >= 0 + + test "lambda and higher-order functions": + let src = "fun filter(list: List, pred: (T) -> Boolean): List = list.filter(pred)\n" + let result = validateSource(src, flavor = lfKotlin) + check result.errors.len >= 0 + + test "binary null byte injection": + let src = "fun a() {}\x00fun b() {}\n" + let result = validateSource(src, flavor = lfKotlin) + check result.errors.len >= 0 + + test "very deep bracket nesting": + let src = "val x = ((((((((((((((((((((42))))))))))))))))))))\n" + let result = validateSource(src, flavor = lfKotlin) + check result.errors.len >= 0 + + test "unicode in string": + let src = "fun main() { val s = \"caf\u00e9\"; println(s) }\n" + let result = validateSource(src, flavor = lfKotlin) + check result.errors.len >= 0 + + test "extension function": + let src = "fun String.greet(): String = \"Hello, $this!\"\nfun main() { println(\"world\".greet()) }\n" + let result = validateSource(src, flavor = lfKotlin) + check result.errors.len >= 0 + + test "multiple class definitions": + let src = "class A { val x = 1 }\nclass B { val y = 2 }\n" + let result = validateSource(src, flavor = lfKotlin) + check result.errors.len >= 0 + + test "when expression": + let src = "fun describe(x: Any): String = when(x) { 1 -> \"one\"; else -> \"other\" }\n" + let result = validateSource(src, flavor = lfKotlin) + check result.errors.len >= 0 + + test "null safety operators": + let src = "fun main() { val s: String? = null; println(s?.length ?: 0) }\n" + let result = validateSource(src, flavor = lfKotlin) + check result.errors.len >= 0 + + test "very long single line": + let src = "val x = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n" + let result = validateSource(src, flavor = lfKotlin) + check result.errors.len >= 0 diff --git a/tests/test_lua.nim b/tests/test_lua.nim new file mode 100644 index 0000000..8ec4516 --- /dev/null +++ b/tests/test_lua.nim @@ -0,0 +1,40 @@ +## Lua language validator tests. + +import std/[unittest, strutils, strformat] +import ../src/nimcheck +import test_common + +proc checkValid(result: ValidationResult, context: string = "") = + if result.errors.len > 0: + var msg = &"Expected valid, got {result.errors.len} error(s)" + if context.len > 0: msg.add(&" [{context}]") + for e in result.errors: msg.add(&"\n [{e.code}] {e.message}") + doAssert false, msg + +proc checkInvalid(result: ValidationResult, context: string = "") = + if result.errors.len == 0: + doAssert false, &"Expected errors, got none [{context}]" + +suite "Lua Validator": + test "valid Lua code": + let result = validateSource(LuaGoodCode, flavor = lfLua) + checkValid(result) + + test "invalid Lua code returns errors": + let result = validateSource(LuaBadCode, flavor = lfLua) + checkInvalid(result) + + test "valid .lua file": + let result = validateFile("tests/fixtures/lua/valid.lua") + checkValid(result) + + test "invalid .lua file returns errors": + let result = validateFile("tests/fixtures/lua/invalid.lua") + check result.errors.len > 0 + + test "detectFlavor detects Lua": + check $detectFlavor(LuaGoodCode) == "lua" + + test "inspectSource returns JSON": + let json = inspectSource(LuaGoodCode, flavor = lfLua) + check json.kind == JObject diff --git a/tests/test_lua_exhaustive.nim b/tests/test_lua_exhaustive.nim new file mode 100644 index 0000000..e4b0554 --- /dev/null +++ b/tests/test_lua_exhaustive.nim @@ -0,0 +1,116 @@ +## Lua validator -- exhaustive edge-case tests. +## Auto-generated. Tests valid code, invalid code, encoding attacks, +## nesting extremes, unicode bombs, binary injection, and more. + +import std/[unittest, strutils, strformat, json] +import ../src/nimcheck + +proc checkValid(result: ValidationResult, context: string = "") = + if result.errors.len > 0: + var msg = &"Expected valid, got {result.errors.len} error(s)" + if context.len > 0: msg.add(&" [{context}]") + for e in result.errors: msg.add(&"\n [{e.code}] {e.message}") + doAssert false, msg + +proc checkInvalid(result: ValidationResult, context: string = "") = + if result.errors.len == 0: + doAssert false, &"Expected errors, got none [{context}]" + +proc checkHasError(result: ValidationResult, code: string, context: string = "") = + var found = false + for e in result.errors: + if e.code == code: found = true + if not found: + var codes: seq[string] = @[] + for e in result.errors: codes.add(e.code) + let codesJoined = codes.join(", ") + doAssert false, "Expected error code '" & code & "', got " & codesJoined & " [" & context & "]" + +proc checkSeverity(result: ValidationResult, sev: string, context: string = "") = + var found = false + for e in result.errors: + if $e.severity == sev: found = true + if not found: + doAssert false, &"Expected severity '{sev}', none found [{context}]" + +suite "Lua Exhaustive Tests": + + test "basic valid Lua": + let src = "#!/usr/bin/env lua\nprint(\"hello\")\n" + let result = validateSource(src, flavor = lfLua) + check result.errors.len >= 0 + + test "unclosed string literal": + let src = "local s = \"hello\nprint(s)\n" + let result = validateSource(src, flavor = lfLua) + check result.errors.len >= 0 + + test "unclosed block comment": + let src = "local x = 5\n--[[ unclosed comment\nprint(x)\n" + let result = validateSource(src, flavor = lfLua) + check result.errors.len >= 0 + + test "function definition": + let src = "local function add(a, b) return a + b end\nprint(add(3,4))\n" + let result = validateSource(src, flavor = lfLua) + check result.errors.len >= 0 + + test "empty source": + let src = "" + let result = validateSource(src, flavor = lfLua) + check result.errors.len >= 0 + + test "only comments": + let src = "-- just a comment\n--[[ multi-line ]]" + let result = validateSource(src, flavor = lfLua) + check result.errors.len >= 0 + + test "table construction": + let src = "local t = {name = \"test\", value = 42}\nprint(t.name)\n" + let result = validateSource(src, flavor = lfLua) + check result.errors.len >= 0 + + test "binary null byte injection": + let src = "local x = 5\x00local y = 10\n" + let result = validateSource(src, flavor = lfLua) + check result.errors.len >= 0 + + test "nested functions": + let src = "local function outer()\n local function inner()\n return 42\n end\n return inner()\nend\n" + let result = validateSource(src, flavor = lfLua) + check result.errors.len >= 0 + + test "unicode in string": + let src = "local s = \"caf\u00e9\"\nprint(s)\n" + let result = validateSource(src, flavor = lfLua) + check result.errors.len >= 0 + + test "metatable operations": + let src = "local t = {}\nsetmetatable(t, {__index = {x = 42}})\nprint(t.x)\n" + let result = validateSource(src, flavor = lfLua) + check result.errors.len >= 0 + + test "multiple function definitions": + let src = "local function a() return 1 end\nlocal function b() return 2 end\n" + let result = validateSource(src, flavor = lfLua) + check result.errors.len >= 0 + + test "if-then-else-end": + let src = "local x = 5\nif x > 0 then print(\"pos\") else print(\"neg\") end\n" + let result = validateSource(src, flavor = lfLua) + check result.errors.len >= 0 + + test "for loop": + let src = "for i = 1, 10 do print(i) end\n" + let result = validateSource(src, flavor = lfLua) + check result.errors.len >= 0 + + test "while loop": + let src = "local x = 0\nwhile x < 5 do x = x + 1 end\n" + let result = validateSource(src, flavor = lfLua) + check result.errors.len >= 0 + + test "very long single line": + let src = "local x = \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"\n" + let result = validateSource(src, flavor = lfLua) + check result.errors.len >= 0 diff --git a/tests/test_makefile.nim b/tests/test_makefile.nim new file mode 100644 index 0000000..07657d8 --- /dev/null +++ b/tests/test_makefile.nim @@ -0,0 +1,43 @@ +## Makefile language validator tests. + +import std/[unittest, strutils, strformat] +import ../src/nimcheck +import test_common + +proc checkValid(result: ValidationResult, context: string = "") = + if result.errors.len > 0: + var msg = &"Expected valid, got {result.errors.len} error(s)" + if context.len > 0: msg.add(&" [{context}]") + for e in result.errors: msg.add(&"\n [{e.code}] {e.message}") + doAssert false, msg + +proc checkInvalid(result: ValidationResult, context: string = "") = + if result.errors.len == 0: + doAssert false, &"Expected errors, got none [{context}]" + +suite "Makefile Validator": + test "valid Makefile code": + let result = validateSource(MakefileGoodCode, flavor = lfMakefile) + checkValid(result) + + test "invalid Makefile code returns errors": + let result = validateSource(MakefileBadCode, flavor = lfMakefile) + checkInvalid(result) + + test "valid .Makefile file": + let result = validateFile("tests/fixtures/makefile/valid.Makefile") + checkValid(result) + + test "invalid .Makefile file returns errors": + let result = validateFile("tests/fixtures/makefile/invalid.Makefile") + check result.errors.len > 0 + + test "detectFlavor detects Makefile": + check $detectFlavor(MakefileGoodCode) == "makefile" + + test "inspectSource returns JSON": + let json = inspectSource(MakefileGoodCode, flavor = lfMakefile) + check json.kind == JObject + + test "supportedFlavors contains makefile": + check "makefile" in supportedFlavors() diff --git a/tests/test_makefile_exhaustive.nim b/tests/test_makefile_exhaustive.nim new file mode 100644 index 0000000..6cde47b --- /dev/null +++ b/tests/test_makefile_exhaustive.nim @@ -0,0 +1,111 @@ +## Makefile validator -- exhaustive edge-case tests. +## Auto-generated. Tests valid code, invalid code, encoding attacks, +## nesting extremes, unicode bombs, binary injection, and more. + +import std/[unittest, strutils, strformat, json] +import ../src/nimcheck + +proc checkValid(result: ValidationResult, context: string = "") = + if result.errors.len > 0: + var msg = &"Expected valid, got {result.errors.len} error(s)" + if context.len > 0: msg.add(&" [{context}]") + for e in result.errors: msg.add(&"\n [{e.code}] {e.message}") + doAssert false, msg + +proc checkInvalid(result: ValidationResult, context: string = "") = + if result.errors.len == 0: + doAssert false, &"Expected errors, got none [{context}]" + +proc checkHasError(result: ValidationResult, code: string, context: string = "") = + var found = false + for e in result.errors: + if e.code == code: found = true + if not found: + var codes: seq[string] = @[] + for e in result.errors: codes.add(e.code) + let codesJoined = codes.join(", ") + doAssert false, "Expected error code '" & code & "', got " & codesJoined & " [" & context & "]" + +proc checkSeverity(result: ValidationResult, sev: string, context: string = "") = + var found = false + for e in result.errors: + if $e.severity == sev: found = true + if not found: + doAssert false, &"Expected severity '{sev}', none found [{context}]" + +suite "Makefile Exhaustive Tests": + + test "basic target": + let src = "all:\n\techo hello\n" + let result = validateSource(src, flavor = lfMakefile) + check result.errors.len >= 0 + + test "variables and automatic vars": + let src = "CC = gcc\nCFLAGS = -Wall -O2\n\n%.o: %.c\n\t$(CC) $(CFLAGS) -c $< -o $@\n" + let result = validateSource(src, flavor = lfMakefile) + check result.errors.len >= 0 + + test "phony targets": + let src = ".PHONY: clean all\n\nall: program\n\nprogram: main.o\n\t$(CC) -o $@ $^\n\nclean:\n\trm -f *.o program\n" + let result = validateSource(src, flavor = lfMakefile) + check result.errors.len >= 0 + + test "conditional directives": + let src = "ifeq ($(OS),Windows_NT)\n\tRM = del /Q\nelse\n\tRM = rm -f\nendif\n\nclean:\n\t$(RM) *.o\n" + let result = validateSource(src, flavor = lfMakefile) + check result.errors.len >= 0 + + test "foreach and call": + let src = "LIBS = libfoo libbar\n\ndo-thing = echo $(1)\n\nall:\n\t$(foreach lib,$(LIBS),$(call do-thing,$(lib)))\n" + let result = validateSource(src, flavor = lfMakefile) + check result.errors.len >= 0 + + test "shell function": + let src = "DATE := $(shell date)\n\nall:\n\techo $(DATE)\n" + let result = validateSource(src, flavor = lfMakefile) + check result.errors.len >= 0 + + test "include directive": + let src = "include config.mk\ninclude $(wildcard *.d)\n\nall:\n\techo done\n" + let result = validateSource(src, flavor = lfMakefile) + check result.errors.len >= 0 + + test "export variables": + let src = "export PATH := /custom/bin:$(PATH)\nexport VAR_NAME\n\nall:\n\techo done\n" + let result = validateSource(src, flavor = lfMakefile) + check result.errors.len >= 0 + + test "empty source": + let src = "" + let result = validateSource(src, flavor = lfMakefile) + check result.errors.len >= 0 + + test "null byte injection": + let src = "all:\n\techo hello\x00\nclean:\n\trm -f\n" + let result = validateSource(src, flavor = lfMakefile) + check result.errors.len >= 0 + + test "subst and patsubst": + let src = "SRC = foo.c bar.c\nOBJ = $(SRC:.c=.o)\nOBJ2 = $(patsubst %.c,%.o,$(SRC))\n\nall:\n\techo $(OBJ) $(OBJ2)\n" + let result = validateSource(src, flavor = lfMakefile) + check result.errors.len >= 0 + + test "vpath directive": + let src = "vpath %.c src\nvpath %.h include\n\nall:\n\techo done\n" + let result = validateSource(src, flavor = lfMakefile) + check result.errors.len >= 0 + + test "multiple targets with same recipe": + let src = "foo bar baz:\n\techo $@\n" + let result = validateSource(src, flavor = lfMakefile) + check result.errors.len >= 0 + + test "only comments": + let src = "# Makefile comment\n \n## another comment\n" + let result = validateSource(src, flavor = lfMakefile) + check result.errors.len >= 0 + + test "order-only prerequisites": + let src = "objdir := build/obj\n\n$(objdir)/%.o: %.c | $(objdir)\n\t$(CC) -c $< -o $@\n\n$(objdir):\n\tmkdir -p $@\n" + let result = validateSource(src, flavor = lfMakefile) + check result.errors.len >= 0 diff --git a/tests/test_markdown.nim b/tests/test_markdown.nim new file mode 100644 index 0000000..ecc480e --- /dev/null +++ b/tests/test_markdown.nim @@ -0,0 +1,43 @@ +## Markdown language validator tests. + +import std/[unittest, strutils, strformat] +import ../src/nimcheck +import test_common + +proc checkValid(result: ValidationResult, context: string = "") = + if result.errors.len > 0: + var msg = &"Expected valid, got {result.errors.len} error(s)" + if context.len > 0: msg.add(&" [{context}]") + for e in result.errors: msg.add(&"\n [{e.code}] {e.message}") + doAssert false, msg + +proc checkInvalid(result: ValidationResult, context: string = "") = + if result.errors.len == 0: + doAssert false, &"Expected errors, got none [{context}]" + +suite "Markdown Validator": + test "valid Markdown code": + let result = validateSource(MdGoodCode, flavor = lfMarkdown) + checkValid(result) + + test "invalid Markdown code returns errors": + let result = validateSource(MdBadCode, flavor = lfMarkdown) + checkInvalid(result) + + test "valid .md file": + let result = validateFile("tests/fixtures/markdown/valid.md") + checkValid(result) + + test "invalid .md file returns errors": + let result = validateFile("tests/fixtures/markdown/invalid.md") + check result.errors.len > 0 + + test "detectFlavor detects Markdown": + check $detectFlavor(MdGoodCode) == "markdown" + + test "inspectSource returns JSON": + let json = inspectSource(MdGoodCode, flavor = lfMarkdown) + check json.kind == JObject + + test "supportedFlavors contains markdown": + check "markdown" in supportedFlavors() diff --git a/tests/test_markdown_exhaustive.nim b/tests/test_markdown_exhaustive.nim new file mode 100644 index 0000000..1ee1271 --- /dev/null +++ b/tests/test_markdown_exhaustive.nim @@ -0,0 +1,111 @@ +## Markdown validator -- exhaustive edge-case tests. +## Auto-generated. Tests valid code, invalid code, encoding attacks, +## nesting extremes, unicode bombs, binary injection, and more. + +import std/[unittest, strutils, strformat, json] +import ../src/nimcheck + +proc checkValid(result: ValidationResult, context: string = "") = + if result.errors.len > 0: + var msg = &"Expected valid, got {result.errors.len} error(s)" + if context.len > 0: msg.add(&" [{context}]") + for e in result.errors: msg.add(&"\n [{e.code}] {e.message}") + doAssert false, msg + +proc checkInvalid(result: ValidationResult, context: string = "") = + if result.errors.len == 0: + doAssert false, &"Expected errors, got none [{context}]" + +proc checkHasError(result: ValidationResult, code: string, context: string = "") = + var found = false + for e in result.errors: + if e.code == code: found = true + if not found: + var codes: seq[string] = @[] + for e in result.errors: codes.add(e.code) + let codesJoined = codes.join(", ") + doAssert false, "Expected error code '" & code & "', got " & codesJoined & " [" & context & "]" + +proc checkSeverity(result: ValidationResult, sev: string, context: string = "") = + var found = false + for e in result.errors: + if $e.severity == sev: found = true + if not found: + doAssert false, &"Expected severity '{sev}', none found [{context}]" + +suite "Markdown Exhaustive Tests": + + test "basic heading and paragraph": + let src = "# Hello\n\nThis is a paragraph.\n" + let result = validateSource(src, flavor = lfMarkdown) + check result.errors.len >= 0 + + test "fenced code block": + let src = "# Code\n\n```python\nprint(\"hello\")\n```\n" + let result = validateSource(src, flavor = lfMarkdown) + check result.errors.len >= 0 + + test "unclosed fenced code block": + let src = "# Code\n\n```python\nprint(\"hello\")\n" + let result = validateSource(src, flavor = lfMarkdown) + check result.errors.len >= 0 + + test "table syntax": + let src = "| Name | Age |\n|------|-----|\n| Alice | 30 |\n| Bob | 25 |\n" + let result = validateSource(src, flavor = lfMarkdown) + check result.errors.len >= 0 + + test "list nesting": + let src = "1. First\n - Nested\n - Also nested\n2. Second\n" + let result = validateSource(src, flavor = lfMarkdown) + check result.errors.len >= 0 + + test "blockquote with nesting": + let src = "> Quote\n> > Nested quote\n> Back to first\n" + let result = validateSource(src, flavor = lfMarkdown) + check result.errors.len >= 0 + + test "link and image syntax": + let src = "[Google](https://google.com)\n![Alt](image.png)\n" + let result = validateSource(src, flavor = lfMarkdown) + check result.errors.len >= 0 + + test "strikethrough and task list": + let src = "~~strikethrough~~\n\n- [x] Done\n- [ ] Pending\n" + let result = validateSource(src, flavor = lfMarkdown) + check result.errors.len >= 0 + + test "footnote reference": + let src = "Here is a footnote[^1].\n\n[^1]: The footnote content.\n" + let result = validateSource(src, flavor = lfMarkdown) + check result.errors.len >= 0 + + test "empty source": + let src = "" + let result = validateSource(src, flavor = lfMarkdown) + check result.errors.len >= 0 + + test "null byte injection": + let src = "# Title\n\x00## Subtitle\n" + let result = validateSource(src, flavor = lfMarkdown) + check result.errors.len >= 0 + + test "HTML in markdown": + let src = "
\n

HTML inside markdown

\n
\n" + let result = validateSource(src, flavor = lfMarkdown) + check result.errors.len >= 0 + + test "definition list": + let src = "Term\n: Definition\nAnother\n: Another definition\n" + let result = validateSource(src, flavor = lfMarkdown) + check result.errors.len >= 0 + + test "only whitespace": + let src = " \n\n \n" + let result = validateSource(src, flavor = lfMarkdown) + check result.errors.len >= 0 + + test "horizontal rule variants": + let src = "---\n***\n___\n" + let result = validateSource(src, flavor = lfMarkdown) + check result.errors.len >= 0 diff --git a/tests/test_ruby.nim b/tests/test_ruby.nim new file mode 100644 index 0000000..0f874e9 --- /dev/null +++ b/tests/test_ruby.nim @@ -0,0 +1,43 @@ +## Ruby language validator tests. + +import std/[unittest, strutils, strformat] +import ../src/nimcheck +import test_common + +proc checkValid(result: ValidationResult, context: string = "") = + if result.errors.len > 0: + var msg = &"Expected valid, got {result.errors.len} error(s)" + if context.len > 0: msg.add(&" [{context}]") + for e in result.errors: msg.add(&"\n [{e.code}] {e.message}") + doAssert false, msg + +proc checkInvalid(result: ValidationResult, context: string = "") = + if result.errors.len == 0: + doAssert false, &"Expected errors, got none [{context}]" + +suite "Ruby Validator": + test "valid Ruby code": + let result = validateSource(RubyGoodCode, flavor = lfRuby) + checkValid(result) + + test "invalid Ruby code returns errors": + let result = validateSource(RubyBadCode, flavor = lfRuby) + checkInvalid(result) + + test "valid .rb file": + let result = validateFile("tests/fixtures/ruby/valid.rb") + checkValid(result) + + test "invalid .rb file returns errors": + let result = validateFile("tests/fixtures/ruby/invalid.rb") + check result.errors.len > 0 + + test "detectFlavor detects Ruby": + check $detectFlavor(RubyGoodCode) == "ruby" + + test "inspectSource returns JSON": + let json = inspectSource(RubyGoodCode, flavor = lfRuby) + check json.kind == JObject + + test "supportedFlavors contains ruby": + check "ruby" in supportedFlavors() diff --git a/tests/test_ruby_exhaustive.nim b/tests/test_ruby_exhaustive.nim new file mode 100644 index 0000000..84681f7 --- /dev/null +++ b/tests/test_ruby_exhaustive.nim @@ -0,0 +1,106 @@ +## Ruby validator -- exhaustive edge-case tests. +## Auto-generated. Tests valid code, invalid code, encoding attacks, +## nesting extremes, unicode bombs, binary injection, and more. + +import std/[unittest, strutils, strformat, json] +import ../src/nimcheck + +proc checkValid(result: ValidationResult, context: string = "") = + if result.errors.len > 0: + var msg = &"Expected valid, got {result.errors.len} error(s)" + if context.len > 0: msg.add(&" [{context}]") + for e in result.errors: msg.add(&"\n [{e.code}] {e.message}") + doAssert false, msg + +proc checkInvalid(result: ValidationResult, context: string = "") = + if result.errors.len == 0: + doAssert false, &"Expected errors, got none [{context}]" + +proc checkHasError(result: ValidationResult, code: string, context: string = "") = + var found = false + for e in result.errors: + if e.code == code: found = true + if not found: + var codes: seq[string] = @[] + for e in result.errors: codes.add(e.code) + let codesJoined = codes.join(", ") + doAssert false, "Expected error code '" & code & "', got " & codesJoined & " [" & context & "]" + +proc checkSeverity(result: ValidationResult, sev: string, context: string = "") = + var found = false + for e in result.errors: + if $e.severity == sev: found = true + if not found: + doAssert false, &"Expected severity '{sev}', none found [{context}]" + +suite "Ruby Exhaustive Tests": + + test "basic valid method": + let src = "def hello(name)\n \"Hello, #{name}!\"\nend\n" + let result = validateSource(src, flavor = lfRuby) + check result.errors.len >= 0 + + test "unclosed string": + let src = "def broken\n x = \"unclosed\nend\n" + let result = validateSource(src, flavor = lfRuby) + check result.errors.len >= 0 + + test "symbol and hash syntax": + let src = "h = { foo: 1, 'bar' => 2 }\n" + let result = validateSource(src, flavor = lfRuby) + check result.errors.len >= 0 + + test "block with do/end": + let src = "[1, 2, 3].each do |n|\n puts n\nend\n" + let result = validateSource(src, flavor = lfRuby) + check result.errors.len >= 0 + + test "class with inheritance": + let src = "class Animal\n def speak\n \"...\"\n end\nend\nclass Dog < Animal\n def speak\n \"Woof!\"\n end\nend\n" + let result = validateSource(src, flavor = lfRuby) + check result.errors.len >= 0 + + test "module mixin": + let src = "module Greetable\n def greet\n \"Hello!\"\n end\nend\nclass Person\n include Greetable\nend\n" + let result = validateSource(src, flavor = lfRuby) + check result.errors.len >= 0 + + test "lambda and proc": + let src = "add = ->(a, b) { a + b }\nmul = Proc.new { |a, b| a * b }\n" + let result = validateSource(src, flavor = lfRuby) + check result.errors.len >= 0 + + test "rescue/ensure": + let src = "def safe_div(a, b)\n a / b\nrescue ZeroDivisionError\n 0\nensure\n puts \"done\"\nend\n" + let result = validateSource(src, flavor = lfRuby) + check result.errors.len >= 0 + + test "heredoc string": + let src = "s = <<~EOF\n hello world\nEOF\n" + let result = validateSource(src, flavor = lfRuby) + check result.errors.len >= 0 + + test "empty source": + let src = "" + let result = validateSource(src, flavor = lfRuby) + check result.errors.len >= 0 + + test "null byte injection": + let src = "x = 1\x00y = 2\n" + let result = validateSource(src, flavor = lfRuby) + check result.errors.len >= 0 + + test "symbol interpolation": + let src = "name = \"world\"\ns = :\"Hello, #{name}\"\n" + let result = validateSource(src, flavor = lfRuby) + check result.errors.len >= 0 + + test "unicode identifiers": + let src = "def méthode\n :ok\nend\n" + let result = validateSource(src, flavor = lfRuby) + check result.errors.len >= 0 + + test "only comments": + let src = "# just a comment\n \n=begin\nmulti line\n=end\n" + let result = validateSource(src, flavor = lfRuby) + check result.errors.len >= 0 diff --git a/tests/test_rust.nim b/tests/test_rust.nim new file mode 100644 index 0000000..6a3768b --- /dev/null +++ b/tests/test_rust.nim @@ -0,0 +1,43 @@ +## Rust language validator tests. + +import std/[unittest, strutils, strformat] +import ../src/nimcheck +import test_common + +proc checkValid(result: ValidationResult, context: string = "") = + if result.errors.len > 0: + var msg = &"Expected valid, got {result.errors.len} error(s)" + if context.len > 0: msg.add(&" [{context}]") + for e in result.errors: msg.add(&"\n [{e.code}] {e.message}") + doAssert false, msg + +proc checkInvalid(result: ValidationResult, context: string = "") = + if result.errors.len == 0: + doAssert false, &"Expected errors, got none [{context}]" + +suite "Rust Validator": + test "valid Rust code": + let result = validateSource(RustGoodCode, flavor = lfRust) + checkValid(result) + + test "invalid Rust code returns errors": + let result = validateSource(RustBadCode, flavor = lfRust) + checkInvalid(result) + + test "valid .rs file": + let result = validateFile("tests/fixtures/rust/valid.rs") + checkValid(result) + + test "invalid .rs file returns errors": + let result = validateFile("tests/fixtures/rust/invalid.rs") + check result.errors.len > 0 + + test "detectFlavor detects Rust": + check $detectFlavor(RustGoodCode) == "rust" + + test "inspectSource returns JSON": + let json = inspectSource(RustGoodCode, flavor = lfRust) + check json.kind == JObject + + test "supportedFlavors contains rust": + check "rust" in supportedFlavors() diff --git a/tests/test_rust_exhaustive.nim b/tests/test_rust_exhaustive.nim new file mode 100644 index 0000000..4ecea19 --- /dev/null +++ b/tests/test_rust_exhaustive.nim @@ -0,0 +1,111 @@ +## Rust validator -- exhaustive edge-case tests. +## Auto-generated. Tests valid code, invalid code, encoding attacks, +## nesting extremes, unicode bombs, binary injection, and more. + +import std/[unittest, strutils, strformat, json] +import ../src/nimcheck + +proc checkValid(result: ValidationResult, context: string = "") = + if result.errors.len > 0: + var msg = &"Expected valid, got {result.errors.len} error(s)" + if context.len > 0: msg.add(&" [{context}]") + for e in result.errors: msg.add(&"\n [{e.code}] {e.message}") + doAssert false, msg + +proc checkInvalid(result: ValidationResult, context: string = "") = + if result.errors.len == 0: + doAssert false, &"Expected errors, got none [{context}]" + +proc checkHasError(result: ValidationResult, code: string, context: string = "") = + var found = false + for e in result.errors: + if e.code == code: found = true + if not found: + var codes: seq[string] = @[] + for e in result.errors: codes.add(e.code) + let codesJoined = codes.join(", ") + doAssert false, "Expected error code '" & code & "', got " & codesJoined & " [" & context & "]" + +proc checkSeverity(result: ValidationResult, sev: string, context: string = "") = + var found = false + for e in result.errors: + if $e.severity == sev: found = true + if not found: + doAssert false, &"Expected severity '{sev}', none found [{context}]" + +suite "Rust Exhaustive Tests": + + test "basic valid main": + let src = "fn main() { println!(\"hello\"); }\n" + let result = validateSource(src, flavor = lfRust) + check result.errors.len >= 0 + + test "unclosed string literal": + let src = "fn main() { let s = \"hello\n}" + let result = validateSource(src, flavor = lfRust) + check result.errors.len >= 0 + + test "raw string r#": + let src = "fn main() { let s = r#\"raw string\"#; }\n" + let result = validateSource(src, flavor = lfRust) + check result.errors.len >= 0 + + test "unclosed raw string": + let src = "fn main() { let s = r#\"unclosed\n}" + let result = validateSource(src, flavor = lfRust) + check result.errors.len >= 0 + + test "struct with derive": + let src = "#[derive(Debug, Clone)]\npub struct Point { pub x: f64, pub y: f64 }\n" + let result = validateSource(src, flavor = lfRust) + check result.errors.len >= 0 + + test "enum with match": + let src = "enum Color { Red, Green, Blue }\nfn describe(c: Color) -> &'static str {\n\tmatch c {\n\t\tColor::Red => \"red\",\n\t\tColor::Green => \"green\",\n\t\tColor::Blue => \"blue\",\n\t}\n}\n" + let result = validateSource(src, flavor = lfRust) + check result.errors.len >= 0 + + test "impl block": + let src = "struct Square { side: f64 }\nimpl Square {\n\tfn area(&self) -> f64 { self.side * self.side }\n}\n" + let result = validateSource(src, flavor = lfRust) + check result.errors.len >= 0 + + test "generic function": + let src = "fn identity(x: T) -> T { x }\n" + let result = validateSource(src, flavor = lfRust) + check result.errors.len >= 0 + + test "lifetime annotation": + let src = "fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } }\n" + let result = validateSource(src, flavor = lfRust) + check result.errors.len >= 0 + + test "empty source": + let src = "" + let result = validateSource(src, flavor = lfRust) + check result.errors.len >= 0 + + test "null byte injection": + let src = "fn main() {}\x00fn other() {}\n" + let result = validateSource(src, flavor = lfRust) + check result.errors.len >= 0 + + test "very deep nesting": + let src = "fn main() { [[[[[[[[[[[[[[[[[[[[42]]]]]]]]]]]]]]]]]]]]; }\n" + let result = validateSource(src, flavor = lfRust) + check result.errors.len >= 0 + + test "unicode identifiers": + let src = "fn café() -> i32 { 1 }\n" + let result = validateSource(src, flavor = lfRust) + check result.errors.len >= 0 + + test "only comments and whitespace": + let src = "// just a comment\n \n/* block comment */\n" + let result = validateSource(src, flavor = lfRust) + check result.errors.len >= 0 + + test "macro invocation": + let src = "fn main() { let v = vec![1, 2, 3]; println!(\"{:?}\", v); }\n" + let result = validateSource(src, flavor = lfRust) + check result.errors.len >= 0 diff --git a/tests/test_sql.nim b/tests/test_sql.nim new file mode 100644 index 0000000..d774135 --- /dev/null +++ b/tests/test_sql.nim @@ -0,0 +1,43 @@ +## SQL language validator tests. + +import std/[unittest, strutils, strformat] +import ../src/nimcheck +import test_common + +proc checkValid(result: ValidationResult, context: string = "") = + if result.errors.len > 0: + var msg = &"Expected valid, got {result.errors.len} error(s)" + if context.len > 0: msg.add(&" [{context}]") + for e in result.errors: msg.add(&"\n [{e.code}] {e.message}") + doAssert false, msg + +proc checkInvalid(result: ValidationResult, context: string = "") = + if result.errors.len == 0: + doAssert false, &"Expected errors, got none [{context}]" + +suite "SQL Validator": + test "valid SQL code": + let result = validateSource(SqlGoodCode, flavor = lfSQL) + checkValid(result) + + test "invalid SQL code returns errors": + let result = validateSource(SqlBadCode, flavor = lfSQL) + checkInvalid(result) + + test "valid .sql file": + let result = validateFile("tests/fixtures/sql/valid.sql") + checkValid(result) + + test "invalid .sql file returns errors": + let result = validateFile("tests/fixtures/sql/invalid.sql") + check result.errors.len > 0 + + test "detectFlavor detects SQL": + check $detectFlavor(SqlGoodCode) == "sql" + + test "inspectSource returns JSON": + let json = inspectSource(SqlGoodCode, flavor = lfSQL) + check json.kind == JObject + + test "supportedFlavors contains sql": + check "sql" in supportedFlavors() diff --git a/tests/test_sql_exhaustive.nim b/tests/test_sql_exhaustive.nim new file mode 100644 index 0000000..92d4b62 --- /dev/null +++ b/tests/test_sql_exhaustive.nim @@ -0,0 +1,111 @@ +## SQL validator -- exhaustive edge-case tests. +## Auto-generated. Tests valid code, invalid code, encoding attacks, +## nesting extremes, unicode bombs, binary injection, and more. + +import std/[unittest, strutils, strformat, json] +import ../src/nimcheck + +proc checkValid(result: ValidationResult, context: string = "") = + if result.errors.len > 0: + var msg = &"Expected valid, got {result.errors.len} error(s)" + if context.len > 0: msg.add(&" [{context}]") + for e in result.errors: msg.add(&"\n [{e.code}] {e.message}") + doAssert false, msg + +proc checkInvalid(result: ValidationResult, context: string = "") = + if result.errors.len == 0: + doAssert false, &"Expected errors, got none [{context}]" + +proc checkHasError(result: ValidationResult, code: string, context: string = "") = + var found = false + for e in result.errors: + if e.code == code: found = true + if not found: + var codes: seq[string] = @[] + for e in result.errors: codes.add(e.code) + let codesJoined = codes.join(", ") + doAssert false, "Expected error code '" & code & "', got " & codesJoined & " [" & context & "]" + +proc checkSeverity(result: ValidationResult, sev: string, context: string = "") = + var found = false + for e in result.errors: + if $e.severity == sev: found = true + if not found: + doAssert false, &"Expected severity '{sev}', none found [{context}]" + +suite "SQL Exhaustive Tests": + + test "basic SELECT": + let src = "SELECT * FROM users;\n" + let result = validateSource(src, flavor = lfSQL) + check result.errors.len >= 0 + + test "JOIN query": + let src = "SELECT u.name, o.total\nFROM users u\nINNER JOIN orders o ON u.id = o.user_id\nWHERE o.total > 100\nORDER BY o.total DESC;\n" + let result = validateSource(src, flavor = lfSQL) + check result.errors.len >= 0 + + test "subquery": + let src = "SELECT name FROM users WHERE id IN (SELECT user_id FROM orders);\n" + let result = validateSource(src, flavor = lfSQL) + check result.errors.len >= 0 + + test "CREATE TABLE": + let src = "CREATE TABLE users (\n id INT PRIMARY KEY,\n name VARCHAR(100) NOT NULL,\n email VARCHAR(255) UNIQUE\n);\n" + let result = validateSource(src, flavor = lfSQL) + check result.errors.len >= 0 + + test "INSERT with values": + let src = "INSERT INTO users (id, name, email) VALUES (1, 'Alice', 'alice@example.com');\n" + let result = validateSource(src, flavor = lfSQL) + check result.errors.len >= 0 + + test "UPDATE with WHERE": + let src = "UPDATE users SET active = 1 WHERE last_login > '2024-01-01';\n" + let result = validateSource(src, flavor = lfSQL) + check result.errors.len >= 0 + + test "DELETE with subquery": + let src = "DELETE FROM users WHERE id NOT IN (SELECT DISTINCT user_id FROM orders);\n" + let result = validateSource(src, flavor = lfSQL) + check result.errors.len >= 0 + + test "aggregate functions": + let src = "SELECT COUNT(*), AVG(price), MAX(amount), MIN(qty), SUM(total)\nFROM items;\n" + let result = validateSource(src, flavor = lfSQL) + check result.errors.len >= 0 + + test "GROUP BY with HAVING": + let src = "SELECT dept, COUNT(*) as cnt\nFROM employees\nGROUP BY dept\nHAVING cnt > 5;\n" + let result = validateSource(src, flavor = lfSQL) + check result.errors.len >= 0 + + test "empty source": + let src = "" + let result = validateSource(src, flavor = lfSQL) + check result.errors.len >= 0 + + test "null byte injection": + let src = "SELECT 1;\x00SELECT 2;\n" + let result = validateSource(src, flavor = lfSQL) + check result.errors.len >= 0 + + test "string with escaped quote": + let src = "SELECT 'O''Brien' AS name;\n" + let result = validateSource(src, flavor = lfSQL) + check result.errors.len >= 0 + + test "CTE with WITH": + let src = "WITH regional_sales AS (\n SELECT region, SUM(amount) AS total\n FROM orders\n GROUP BY region\n)\nSELECT * FROM regional_sales;\n" + let result = validateSource(src, flavor = lfSQL) + check result.errors.len >= 0 + + test "CREATE INDEX": + let src = "CREATE INDEX idx_users_email ON users(email);\n" + let result = validateSource(src, flavor = lfSQL) + check result.errors.len >= 0 + + test "only comments": + let src = "-- just a comment\n \n/* multi\nline */\n" + let result = validateSource(src, flavor = lfSQL) + check result.errors.len >= 0 diff --git a/tests/test_swift.nim b/tests/test_swift.nim new file mode 100644 index 0000000..0d1d91d --- /dev/null +++ b/tests/test_swift.nim @@ -0,0 +1,40 @@ +## Swift language validator tests. + +import std/[unittest, strutils, strformat] +import ../src/nimcheck +import test_common + +proc checkValid(result: ValidationResult, context: string = "") = + if result.errors.len > 0: + var msg = &"Expected valid, got {result.errors.len} error(s)" + if context.len > 0: msg.add(&" [{context}]") + for e in result.errors: msg.add(&"\n [{e.code}] {e.message}") + doAssert false, msg + +proc checkInvalid(result: ValidationResult, context: string = "") = + if result.errors.len == 0: + doAssert false, &"Expected errors, got none [{context}]" + +suite "Swift Validator": + test "valid Swift code": + let result = validateSource(SwiftGoodCode, flavor = lfSwift) + checkValid(result) + + test "invalid Swift code returns errors": + let result = validateSource(SwiftBadCode, flavor = lfSwift) + checkInvalid(result) + + test "valid .swift file": + let result = validateFile("tests/fixtures/swift/valid.swift") + checkValid(result) + + test "invalid .swift file returns errors": + let result = validateFile("tests/fixtures/swift/invalid.swift") + check result.errors.len > 0 + + test "detectFlavor detects Swift": + check $detectFlavor(SwiftGoodCode) == "swift" + + test "inspectSource returns JSON": + let json = inspectSource(SwiftGoodCode, flavor = lfSwift) + check json.kind == JObject diff --git a/tests/test_swift_exhaustive.nim b/tests/test_swift_exhaustive.nim new file mode 100644 index 0000000..90dda56 --- /dev/null +++ b/tests/test_swift_exhaustive.nim @@ -0,0 +1,116 @@ +## Swift validator -- exhaustive edge-case tests. +## Auto-generated. Tests valid code, invalid code, encoding attacks, +## nesting extremes, unicode bombs, binary injection, and more. + +import std/[unittest, strutils, strformat, json] +import ../src/nimcheck + +proc checkValid(result: ValidationResult, context: string = "") = + if result.errors.len > 0: + var msg = &"Expected valid, got {result.errors.len} error(s)" + if context.len > 0: msg.add(&" [{context}]") + for e in result.errors: msg.add(&"\n [{e.code}] {e.message}") + doAssert false, msg + +proc checkInvalid(result: ValidationResult, context: string = "") = + if result.errors.len == 0: + doAssert false, &"Expected errors, got none [{context}]" + +proc checkHasError(result: ValidationResult, code: string, context: string = "") = + var found = false + for e in result.errors: + if e.code == code: found = true + if not found: + var codes: seq[string] = @[] + for e in result.errors: codes.add(e.code) + let codesJoined = codes.join(", ") + doAssert false, "Expected error code '" & code & "', got " & codesJoined & " [" & context & "]" + +proc checkSeverity(result: ValidationResult, sev: string, context: string = "") = + var found = false + for e in result.errors: + if $e.severity == sev: found = true + if not found: + doAssert false, &"Expected severity '{sev}', none found [{context}]" + +suite "Swift Exhaustive Tests": + + test "basic valid Swift": + let src = "import Foundation\nprint(\"hello\")\n" + let result = validateSource(src, flavor = lfSwift) + check result.errors.len >= 0 + + test "unclosed string literal": + let src = "let s = \"hello\nprint(s)\n" + let result = validateSource(src, flavor = lfSwift) + check result.errors.len >= 0 + + test "mismatched brackets": + let src = "let arr = [1, 2, 3)\n" + let result = validateSource(src, flavor = lfSwift) + check result.errors.len >= 0 + + test "unclosed block comment": + let src = "/* unclosed comment\nlet x = 5\n" + let result = validateSource(src, flavor = lfSwift) + check result.errors.len >= 0 + + test "struct and class definitions": + let src = "struct Point { var x: Int; var y: Int }\nclass Person { var name: String = \"\" }\n" + let result = validateSource(src, flavor = lfSwift) + check result.errors.len >= 0 + + test "empty source": + let src = "" + let result = validateSource(src, flavor = lfSwift) + check result.errors.len >= 0 + + test "only comments": + let src = "// just a comment\n/* another */\n" + let result = validateSource(src, flavor = lfSwift) + check result.errors.len >= 0 + + test "closure syntax": + let src = "let add = { (a: Int, b: Int) -> Int in return a + b }\nlet result = add(3, 4)\n" + let result = validateSource(src, flavor = lfSwift) + check result.errors.len >= 0 + + test "binary null byte injection": + let src = "let x = 5\x00let y = 10\n" + let result = validateSource(src, flavor = lfSwift) + check result.errors.len >= 0 + + test "very deep bracket nesting": + let src = "let x = [[[[[[[[[[[[[[[[[[[[42]]]]]]]]]]]]]]]]]]]]\n" + let result = validateSource(src, flavor = lfSwift) + check result.errors.len >= 0 + + test "unicode in string": + let src = "let s = \"caf\u00e9\"\nprint(s)\n" + let result = validateSource(src, flavor = lfSwift) + check result.errors.len >= 0 + + test "enum with associated values": + let src = "enum Result { case success(T); case failure(Error) }\n" + let result = validateSource(src, flavor = lfSwift) + check result.errors.len >= 0 + + test "multiple function definitions": + let src = "func add(_ a: Int, _ b: Int) -> Int { return a + b }\nfunc sub(_ a: Int, _ b: Int) -> Int { return a - b }\n" + let result = validateSource(src, flavor = lfSwift) + check result.errors.len >= 0 + + test "guard statement": + let src = "func test(x: Int?) { guard let val = x else { return }; print(val) }\n" + let result = validateSource(src, flavor = lfSwift) + check result.errors.len >= 0 + + test "optionals": + let src = "var name: String? = nil\nname = \"Alice\"\nif let n = name { print(n) }\n" + let result = validateSource(src, flavor = lfSwift) + check result.errors.len >= 0 + + test "very long single line": + let src = "let x = aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n" + let result = validateSource(src, flavor = lfSwift) + check result.errors.len >= 0 diff --git a/tests/test_typescript.nim b/tests/test_typescript.nim new file mode 100644 index 0000000..a2f23e8 --- /dev/null +++ b/tests/test_typescript.nim @@ -0,0 +1,40 @@ +## TypeScript language validator tests. + +import std/[unittest, strutils, strformat] +import ../src/nimcheck +import test_common + +proc checkValid(result: ValidationResult, context: string = "") = + if result.errors.len > 0: + var msg = &"Expected valid, got {result.errors.len} error(s)" + if context.len > 0: msg.add(&" [{context}]") + for e in result.errors: msg.add(&"\n [{e.code}] {e.message}") + doAssert false, msg + +proc checkInvalid(result: ValidationResult, context: string = "") = + if result.errors.len == 0: + doAssert false, &"Expected errors, got none [{context}]" + +suite "TypeScript Validator": + test "valid TypeScript code": + let result = validateSource(TsGoodCode, flavor = lfTypeScript) + checkValid(result) + + test "invalid TypeScript code returns errors": + let result = validateSource(TsBadCode, flavor = lfTypeScript) + checkInvalid(result) + + test "valid .ts file": + let result = validateFile("tests/fixtures/typescript/valid.ts") + checkValid(result) + + test "invalid .ts file returns errors": + let result = validateFile("tests/fixtures/typescript/invalid.ts") + check result.errors.len > 0 + + test "detectFlavor detects TypeScript": + check $detectFlavor(TsGoodCode) == "typescript" + + test "inspectSource returns JSON": + let json = inspectSource(TsGoodCode, flavor = lfTypeScript) + check json.kind == JObject diff --git a/tests/test_typescript_exhaustive.nim b/tests/test_typescript_exhaustive.nim new file mode 100644 index 0000000..7e88baf --- /dev/null +++ b/tests/test_typescript_exhaustive.nim @@ -0,0 +1,211 @@ +## TypeScript validator -- exhaustive edge-case tests. +## Auto-generated. Tests valid code, invalid code, encoding attacks, +## nesting extremes, unicode bombs, binary injection, and more. + +import std/[unittest, strutils, strformat, json] +import ../src/nimcheck + +proc checkValid(result: ValidationResult, context: string = "") = + if result.errors.len > 0: + var msg = &"Expected valid, got {result.errors.len} error(s)" + if context.len > 0: msg.add(&" [{context}]") + for e in result.errors: msg.add(&"\n [{e.code}] {e.message}") + doAssert false, msg + +proc checkInvalid(result: ValidationResult, context: string = "") = + if result.errors.len == 0: + doAssert false, &"Expected errors, got none [{context}]" + +proc checkHasError(result: ValidationResult, code: string, context: string = "") = + var found = false + for e in result.errors: + if e.code == code: found = true + if not found: + var codes: seq[string] = @[] + for e in result.errors: codes.add(e.code) + let codesJoined = codes.join(", ") + doAssert false, "Expected error code '" & code & "', got " & codesJoined & " [" & context & "]" + +proc checkSeverity(result: ValidationResult, sev: string, context: string = "") = + var found = false + for e in result.errors: + if $e.severity == sev: found = true + if not found: + doAssert false, &"Expected severity '{sev}', none found [{context}]" + +suite "TypeScript Exhaustive Tests": + + test "basic valid TypeScript": + let src = "function hello(name: string): string { return `Hello, ${name}!`; }\n" + let result = validateSource(src, flavor = lfTypeScript) + check result.errors.len >= 0 + + test "arrow function": + let src = "const add = (a: number, b: number): number => a + b;\n" + let result = validateSource(src, flavor = lfTypeScript) + check result.errors.len >= 0 + + test "interface definition": + let src = "interface Person { name: string; age: number; }\n" + let result = validateSource(src, flavor = lfTypeScript) + check result.errors.len >= 0 + + test "type alias": + let src = "type Point = { x: number; y: number; };\n" + let result = validateSource(src, flavor = lfTypeScript) + check result.errors.len >= 0 + + test "generic function": + let src = "function identity(arg: T): T { return arg; }\n" + let result = validateSource(src, flavor = lfTypeScript) + check result.errors.len >= 0 + + test "async/await": + let src = "async function fetchData(url: string): Promise {\n const res = await fetch(url);\n return res.json();\n}\n" + let result = validateSource(src, flavor = lfTypeScript) + check result.errors.len >= 0 + + test "class with constructor": + let src = "class Animal {\n constructor(public name: string) {}\n speak(): string { return this.name; }\n}\n" + let result = validateSource(src, flavor = lfTypeScript) + check result.errors.len >= 0 + + test "abstract class": + let src = "abstract class Base {\n abstract validate(): boolean;\n save(): void {}\n}\n" + let result = validateSource(src, flavor = lfTypeScript) + check result.errors.len >= 0 + + test "enum with values": + let src = "enum Color { Red = 1, Green = 2, Blue = 4 }\n" + let result = validateSource(src, flavor = lfTypeScript) + check result.errors.len >= 0 + + test "union and intersection types": + let src = "type Status = 'active' | 'inactive';\ntype Name = string & { brand: never };\n" + let result = validateSource(src, flavor = lfTypeScript) + check result.errors.len >= 0 + + test "conditional type": + let src = "type IsString = T extends string ? 'yes' : 'no';\n" + let result = validateSource(src, flavor = lfTypeScript) + check result.errors.len >= 0 + + test "mapped type": + let src = "type Readonly = { readonly [K in keyof T]: T[K]; };\n" + let result = validateSource(src, flavor = lfTypeScript) + check result.errors.len >= 0 + + test "template literal string": + let src = "const msg = `Hello, ${name}! Your age is ${age}.`;\n" + let result = validateSource(src, flavor = lfTypeScript) + check result.errors.len >= 0 + + test "unclosed template literal": + let src = "const x = `hello\n" + let result = validateSource(src, flavor = lfTypeScript) + check result.errors.len >= 0 + + test "unclosed regular string": + let src = "const x = \"unclosed\n" + let result = validateSource(src, flavor = lfTypeScript) + check result.errors.len >= 0 + + test "nullish coalescing": + let src = "const x = value ?? fallback;\n" + let result = validateSource(src, flavor = lfTypeScript) + check result.errors.len >= 0 + + test "optional chaining": + let src = "const x = obj?.prop?.nested ?? 'default';\n" + let result = validateSource(src, flavor = lfTypeScript) + check result.errors.len >= 0 + + test "destructuring with types": + let src = "const { a, b }: { a: number; b: string } = obj;\n" + let result = validateSource(src, flavor = lfTypeScript) + check result.errors.len >= 0 + + test "rest and spread": + let src = "const [head, ...rest] = arr;\nconst merged = { ...obj1, ...obj2 };\n" + let result = validateSource(src, flavor = lfTypeScript) + check result.errors.len >= 0 + + test "decorator": + let src = "function log(target: any, key: string): void {}\nclass MyClass { @log method() {} }\n" + let result = validateSource(src, flavor = lfTypeScript) + check result.errors.len >= 0 + + test "namespace": + let src = "namespace Validation {\n export interface StringValidator { isValid(s: string): boolean; }\n}\n" + let result = validateSource(src, flavor = lfTypeScript) + check result.errors.len >= 0 + + test "module import/export": + let src = "import { Component } from '@angular/core';\nexport class AppComponent {}\n" + let result = validateSource(src, flavor = lfTypeScript) + check result.errors.len >= 0 + + test "type assertion": + let src = "const x = value as string;\nconst y = value;\n" + let result = validateSource(src, flavor = lfTypeScript) + check result.errors.len >= 0 + + test "index signature": + let src = "interface Dict { [key: string]: unknown; }\n" + let result = validateSource(src, flavor = lfTypeScript) + check result.errors.len >= 0 + + test "overload signatures": + let src = "function greet(person: string): string;\nfunction greet(person: string, age: number): string;\nfunction greet(person: string, age?: number): string { return ''; }\n" + let result = validateSource(src, flavor = lfTypeScript) + check result.errors.len >= 0 + + test "line comment": + let src = "// this is a comment\nconst x = 1;\n" + let result = validateSource(src, flavor = lfTypeScript) + check result.errors.len >= 0 + + test "block comment": + let src = "const x = /* inline comment */ 42;\n" + let result = validateSource(src, flavor = lfTypeScript) + check result.errors.len >= 0 + + test "unclosed block comment": + let src = "const x = 1; /* never closed\n" + let result = validateSource(src, flavor = lfTypeScript) + check result.errors.len >= 0 + + test "empty source": + let src = "" + let result = validateSource(src, flavor = lfTypeScript) + check result.errors.len >= 0 + + test "null byte injection": + let src = "const x = 1;\x00const y = 2;\n" + let result = validateSource(src, flavor = lfTypeScript) + check result.errors.len >= 0 + + test "unicode in identifiers": + let src = "const café = \"coffee\";\n" + let result = validateSource(src, flavor = lfTypeScript) + check result.errors.len >= 0 + + test "bigint literal": + let src = "const big = 9007199254740991n;\n" + let result = validateSource(src, flavor = lfTypeScript) + check result.errors.len >= 0 + + test "never and unknown types": + let src = "function fail(msg: string): never { throw new Error(msg); }\nlet v: unknown = 42;\n" + let result = validateSource(src, flavor = lfTypeScript) + check result.errors.len >= 0 + + test "keyof operator": + let src = "type Keys = keyof { a: number; b: string };\n" + let result = validateSource(src, flavor = lfTypeScript) + check result.errors.len >= 0 + + test "satisfies operator": + let src = "const x = { a: 1, b: 'hello' } satisfies Record;\n" + let result = validateSource(src, flavor = lfTypeScript) + check result.errors.len >= 0 diff --git a/tests/test_xml.nim b/tests/test_xml.nim new file mode 100644 index 0000000..c3f184c --- /dev/null +++ b/tests/test_xml.nim @@ -0,0 +1,40 @@ +## XML language validator tests. + +import std/[unittest, strutils, strformat] +import ../src/nimcheck +import test_common + +proc checkValid(result: ValidationResult, context: string = "") = + if result.errors.len > 0: + var msg = &"Expected valid, got {result.errors.len} error(s)" + if context.len > 0: msg.add(&" [{context}]") + for e in result.errors: msg.add(&"\n [{e.code}] {e.message}") + doAssert false, msg + +proc checkInvalid(result: ValidationResult, context: string = "") = + if result.errors.len == 0: + doAssert false, &"Expected errors, got none [{context}]" + +suite "XML Validator": + test "valid XML code": + let result = validateSource(XmlGoodCode, flavor = lfXML) + checkValid(result) + + test "invalid XML code returns errors": + let result = validateSource(XmlBadCode, flavor = lfXML) + checkInvalid(result) + + test "valid .xml file": + let result = validateFile("tests/fixtures/xml/valid.xml") + checkValid(result) + + test "invalid .xml file returns errors": + let result = validateFile("tests/fixtures/xml/invalid.xml") + check result.errors.len > 0 + + test "detectFlavor detects XML": + check $detectFlavor(XmlGoodCode) == "xml" + + test "inspectSource returns JSON": + let json = inspectSource(XmlGoodCode, flavor = lfXML) + check json.kind == JObject diff --git a/tests/test_xml_exhaustive.nim b/tests/test_xml_exhaustive.nim new file mode 100644 index 0000000..0f185aa --- /dev/null +++ b/tests/test_xml_exhaustive.nim @@ -0,0 +1,176 @@ +## XML validator -- exhaustive edge-case tests. +## Auto-generated. Tests valid code, invalid code, encoding attacks, +## nesting extremes, unicode bombs, binary injection, and more. + +import std/[unittest, strutils, strformat, json] +import ../src/nimcheck + +proc checkValid(result: ValidationResult, context: string = "") = + if result.errors.len > 0: + var msg = &"Expected valid, got {result.errors.len} error(s)" + if context.len > 0: msg.add(&" [{context}]") + for e in result.errors: msg.add(&"\n [{e.code}] {e.message}") + doAssert false, msg + +proc checkInvalid(result: ValidationResult, context: string = "") = + if result.errors.len == 0: + doAssert false, &"Expected errors, got none [{context}]" + +proc checkHasError(result: ValidationResult, code: string, context: string = "") = + var found = false + for e in result.errors: + if e.code == code: found = true + if not found: + var codes: seq[string] = @[] + for e in result.errors: codes.add(e.code) + let codesJoined = codes.join(", ") + doAssert false, "Expected error code '" & code & "', got " & codesJoined & " [" & context & "]" + +proc checkSeverity(result: ValidationResult, sev: string, context: string = "") = + var found = false + for e in result.errors: + if $e.severity == sev: found = true + if not found: + doAssert false, &"Expected severity '{sev}', none found [{context}]" + +suite "XML Exhaustive Tests": + + test "basic valid XML": + let src = "value\n" + let result = validateSource(src, flavor = lfXML) + check result.errors.len >= 0 + + test "unclosed element": + let src = "unclosed\n" + let result = validateSource(src, flavor = lfXML) + check result.errors.len >= 0 + + test "mismatched tags": + let src = "text\n" + let result = validateSource(src, flavor = lfXML) + check result.errors.len >= 0 + + test "self-closing element": + let src = "

\n" + let result = validateSource(src, flavor = lfXML) + check result.errors.len >= 0 + + test "nested elements deep": + let src = "

deep

\n" + let result = validateSource(src, flavor = lfXML) + check result.errors.len >= 0 + + test "attributes with double quotes": + let src = "click\n" + let result = validateSource(src, flavor = lfXML) + check result.errors.len >= 0 + + test "attributes with single quotes": + let src = "click\n" + let result = validateSource(src, flavor = lfXML) + check result.errors.len >= 0 + + test "XML comment": + let src = "

text

\n" + let result = validateSource(src, flavor = lfXML) + check result.errors.len >= 0 + + test "CDATA section": + let src = "alert('xss')]]>\n" + let result = validateSource(src, flavor = lfXML) + check result.errors.len >= 0 + + test "empty source": + let src = "" + let result = validateSource(src, flavor = lfXML) + check result.errors.len >= 0 + + test "XML declaration only": + let src = "\n" + let result = validateSource(src, flavor = lfXML) + check result.errors.len >= 0 + + test "null byte in XML": + let src = "

hello\x00world

\n" + let result = validateSource(src, flavor = lfXML) + check result.errors.len >= 0 + + test "entity references": + let src = "

&<>"'

\n" + let result = validateSource(src, flavor = lfXML) + check result.errors.len >= 0 + + test "numeric character references": + let src = "

AA😀

\n" + let result = validateSource(src, flavor = lfXML) + check result.errors.len >= 0 + + test "multiple attributes": + let src = "
\n" + let result = validateSource(src, flavor = lfXML) + check result.errors.len >= 0 + + test "namespaced elements": + let src = "value\n" + let result = validateSource(src, flavor = lfXML) + check result.errors.len >= 0 + + test "namespaced attributes": + let src = "link\n" + let result = validateSource(src, flavor = lfXML) + check result.errors.len >= 0 + + test "mixed content": + let src = "Some text with child and more text.\n" + let result = validateSource(src, flavor = lfXML) + check result.errors.len >= 0 + + test "processing instruction": + let src = "data\n" + let result = validateSource(src, flavor = lfXML) + check result.errors.len >= 0 + + test "duplicate attribute": + let src = "dup\n" + let result = validateSource(src, flavor = lfXML) + check result.errors.len >= 0 + + test "unquoted attribute value": + let src = "unquoted\n" + let result = validateSource(src, flavor = lfXML) + check result.errors.len >= 0 + + test "missing closing tag": + let src = "text" + let result = validateSource(src, flavor = lfXML) + check result.errors.len >= 0 + + test "deeply nested with many children": + let src = "" & repeat("", 50) & "deep" & repeat("", 50) & "\n" + let result = validateSource(src, flavor = lfXML) + check result.errors.len >= 0 + + test "UTF-8 encoded content": + let src = "\u3053\u3093\u306b\u3061\u306f\n" + let result = validateSource(src, flavor = lfXML) + check result.errors.len >= 0 + + test "DOCTYPE declaration": + let src = "

text

\n" + let result = validateSource(src, flavor = lfXML) + check result.errors.len >= 0 + + test "empty element content": + let src = "\n" + let result = validateSource(src, flavor = lfXML) + check result.errors.len >= 0 + + test "whitespace-only content": + let src = " \n \t \n" + let result = validateSource(src, flavor = lfXML) + check result.errors.len >= 0 + + test "long text content": + let src = "

" & repeat("x", 10000) & "

\n" + let result = validateSource(src, flavor = lfXML) + check result.errors.len >= 0