module Snake.Tests.Purity open System.IO open System.Text.RegularExpressions open Expecto let private srcDir = Path.Combine(Directory.GetCurrentDirectory(), "src") // The one file allowed to touch the outside world (the browser glue lives in web/, outside src/). let private excluded (file: string) = Path.GetFileName(file) = "Runtime.fs" let private banned: (string * Regex * string) list = [ "mutable", Regex(@"\bmutable\b"), "let mutable n = 0" "<-", Regex(@"<-"), "n <- n + 1" "System.Random", Regex(@"\bSystem\.Random\b"), "let r = System.Random()" "DateTime", Regex(@"\bDateTime\b"), "let t = System.DateTime.Now" "printf", Regex(@"\bprintf"), "printfn \"hi\"" "ref ", Regex(@"\bref "), "let cell = ref 0" ] // Block comments `(* ... *)` and line comments (`//`, `///`) are removed before scanning. let private stripComments (code: string) = Regex.Replace(Regex.Replace(code, @"(?s)\(\*.*?\*\)", ""), @"(?m)//.*$", "") let private violations (code: string) = let clean = stripComments code [ for name, re, _ in banned do if re.IsMatch clean then yield name ] let private fsFiles () = if Directory.Exists srcDir then Directory.GetFiles(srcDir, "*.fs", SearchOption.AllDirectories) |> Array.filter (fun f -> // skip generated output: any path component named bin or obj let rel = Path.GetRelativePath(srcDir, f) not (rel.Split([| '/'; '\\' |]) |> Array.exists (fun d -> d = "bin" || d = "obj"))) |> Array.sort |> List.ofArray else [] [] let tests = testList "Purity" [ for name, _, sample in banned do yield testCase $"purity scanner flags a known-bad sample using {name}" (fun () -> Expect.contains (violations sample) name "flagged") yield testCase "purity scanner ignores banned words inside comments" (fun () -> let code = "// let mutable x\n(* x <- 1; printfn \"hi\" *)\nlet a = 1 // System.Random DateTime ref x\n" Expect.equal (violations code) [] "no violations") yield testCase "purity scanner accepts ordinary pure code" (fun () -> Expect.equal (violations "let f xs = List.map (fun x -> x + 1) xs") [] "no violations") for f in fsFiles () do if not (excluded f) then yield testCase $"purity: {Path.GetRelativePath(srcDir, f)} has no banned constructs" (fun () -> Expect.equal (violations (File.ReadAllText f)) [] "violations") ]