Files
functional-programming-snake/fsharp/tests/Purity.fs
T
2026-10-29 01:43:48 +01:00

61 lines
2.5 KiB
FSharp

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
[]
[<Tests>]
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")
]