Skip to content

Instantly share code, notes, and snippets.

@plainionist
Created June 16, 2026 19:20
Show Gist options
  • Select an option

  • Save plainionist/3f83905f20939f335cafa6f5a2941f40 to your computer and use it in GitHub Desktop.

Select an option

Save plainionist/3f83905f20939f335cafa6f5a2941f40 to your computer and use it in GitHub Desktop.
Architectural fitness function to prevent reflection in tests
namespace Bake.Tasks.ArchitectureFitness
open System
open System.IO
module AF015 =
let private relative root path = Path.GetRelativePath(root, path)
let private reflectionPatterns = [|
".GetType().GetMethod("
".GetType().GetProperty("
".GetType().GetField("
"MethodInfo"
"PropertyInfo"
|]
// Only for TestApi files: opening System.Reflection is itself a signal,
// even without explicit member invocations.
let private testApiReflectionOpen = "open System.Reflection"
let private containsReflection (isTestApi: bool) (text: string) =
reflectionPatterns |> Array.exists (fun p -> text.Contains(p, StringComparison.Ordinal))
|| (isTestApi && text.Contains(testApiReflectionOpen, StringComparison.Ordinal))
let private isTestApiOrSpecsFile (root: string) (path: string) =
let rel = Path.GetRelativePath(root, path)
let fileName = Path.GetFileName(path)
if fileName.Equals("AssemblyInfo.fs", StringComparison.OrdinalIgnoreCase) then false
else
let isTestApi = fileName.Equals("TestApi.fs", StringComparison.OrdinalIgnoreCase)
let parts = rel.Split([| '/'; '\\' |], StringSplitOptions.RemoveEmptyEntries)
let inSpecsProject =
parts.Length >= 2 &&
parts.[1].EndsWith(".Specs", StringComparison.OrdinalIgnoreCase)
isTestApi || inSpecsProject
let private checkFile root (path: string) : Diagnostic option =
let isTestApi = Path.GetFileName(path).Equals("TestApi.fs", StringComparison.OrdinalIgnoreCase)
let text = File.ReadAllText path
if containsReflection isTestApi text then
Some { RuleId = "AF015"
Severity = Error
Path = relative root path
Line = None
Message = "TestApi or spec file uses reflection to reach production code. Use typed calls instead." }
else None
let rule : Rule = {
Id = "AF015"
Title = "TestApi and spec files must not use reflection to reach production code"
SourceDocs = "docs/Manual/Testing/TestApi.md"
Check = fun root ->
let systemDir = Path.Combine(root, "system")
Diagnosis.enumerateFiles systemDir "*.fs"
|> Seq.filter (fun p ->
not (p.Contains(Path.DirectorySeparatorChar.ToString() + "obj" + Path.DirectorySeparatorChar.ToString()) ||
p.Contains(Path.DirectorySeparatorChar.ToString() + "bin" + Path.DirectorySeparatorChar.ToString())))
|> Seq.filter (isTestApiOrSpecsFile systemDir)
|> Seq.choose (checkFile root)
|> List.ofSeq
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment