61 lines
2.5 KiB
FSharp
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")
|
|
]
|