Created
June 16, 2026 19:20
-
-
Save plainionist/3f83905f20939f335cafa6f5a2941f40 to your computer and use it in GitHub Desktop.
Architectural fitness function to prevent reflection in tests
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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