Repository F# setup
open System
open System.IO
open System.Threading
open System.Threading.Tasks
open Axial
open Axial.Layers
open Axial.Console
open Axial.FileSystem
open Axial.Hosting
open Axial.Hosting.Browser
open Axial.Hosting.Node
open Axial.PlatformService
open Axial.State
open Axial.Telemetry
open Axial.Telemetry.JavaScript

Write an effect-boundary analyzer

Effect-boundary guardrails describes how Axial.Guardrails detects direct calls to ambient .NET effects. You can apply the same pattern to an effect owned by your package.

For example, callers can bypass an HTTP, queue, or database service interface by using the underlying .NET API. A package-specific analyzer can detect that access during the build.

Create an analyzer after your package defines an explicit service boundary for consumers.

Copy the reusable components

FSharp.Analyzers.SDK discovers each analyzer assembly independently. Axial.Guardrails does not provide a plug-in API, and your analyzer does not need to reference it.

Copy these files from the src/Axial.Guardrails directory in the Axial source tree. The published docs and NuGet package do not ship these files, so fetch them from the repository:

Together, these files contain about 150 lines. They use FSharpMemberOrFunctionOrValue from FSharp.Compiler.Symbols and do not depend on Axial types.

Define a symbol rule

Suppose your package exposes IHttp and must detect direct construction of System.Net.Http.HttpClient.

Define a rule for the constructor:

Fragment
module Contoso.Http.Guardrails.EffectCatalog

type SymbolMatch =
    | ConstructorOf of entityFullName: string

type EffectRule =
    { Category: string
      Match: SymbolMatch
      Message: string
      Replacement: string }

let rules: EffectRule list =
    [ { Category = "http"
        Match = ConstructorOf "System.Net.Http.HttpClient"
        Message = "constructs System.Net.Http.HttpClient directly, bypassing the explicit HTTP service"
        Replacement = "Contoso.Http's IHttp service" } ]

let knownCategories: Set<string> =
    rules |> List.map _.Category |> Set.ofList

Copy Suppressions.fs, then replace the Axial directive prefix with a package-specific prefix such as contoso-http-allow. A distinct prefix prevents confusion with suppressions from other analyzers.

Copy EffectBoundaryAnalyzer.fs, then change the analyzer name, diagnostic code, and help URL:

Fragment
[<CliAnalyzer("ContosoHttpBoundary",
              "Flags direct construction of System.Net.Http.HttpClient that bypasses IHttp.",
              "https://github.com/contoso/http/blob/main/docs/guardrails.md")>]
let httpBoundaryAnalyzer: Analyzer<CliContext> =
    fun ctx -> async {
        let fileCategories = fileLevelAllowedCategories ctx.SourceText

        return
            ctx.GetAllSymbolUsesOfFile()
            |> Seq.choose (fun symbolUse ->
                ruleFor symbolUse.Symbol
                |> Option.map (fun rule -> rule, symbolUse.Range))
            |> Seq.filter (fun (rule, range) ->
                not (isAllowed ctx.SourceText fileCategories range.StartLine rule.Category))
            |> Seq.map (fun (rule, range) -> toMessage rule range)
            |> Seq.toList
    }

A matching call produces a diagnostic identifying the direct call and the replacement service.

Verify a consumer build

After packaging, confirm three behaviors in one consumer project: the analyzer flags the direct call, a suppression silences it, and error severity fails the build. Create a consumer that references the analyzer package and builds the direct call:

Fragment
module Consumer
let makeClient () = new System.Net.Http.HttpClient()

Build with severity error. The build exits non-zero and reports the ContosoHttpBoundary diagnostic naming the System.Net.Http.HttpClient constructor and the IHttp replacement. Then mark the boundary and rebuild; the build now succeeds:

Fragment
module Consumer
let makeClient () = new System.Net.Http.HttpClient() // contoso-http-allow: http

Run the same pair for every rule the analyzer ships, once at warning (build succeeds, finding reported) and once at error (build fails). tests/Axial.Guardrails.Tests/PackagePropsTests.fs is the reference implementation of this loop: it writes a consumer into a temp directory, runs dotnet build, and asserts on the exit code and the reported AXG001/AXG002 findings.

Package the analyzer

Packaging — the NuGet layout, PackageDownload for the CLI, and the MSBuild props/targets that run it — is documented in the analyzer packaging reference. In short: pack the analyzer's analyzers/ and cli/ directories into the package, add build/{YourPackageId}.props and build/{YourPackageId}.targets, and run the CLI with an <Exec> task that can fail the build.

Keep the analyzer independent

Start by copying the three source files and packaging configuration into your repository. Do not add an Axial.Guardrails or other Axial.* dependency.

The implementation is a template, not a plug-in API. Keeping the analyzer independent also prevents consumers from acquiring an unrelated Axial dependency.

Extract shared analyzer infrastructure if several analyzers need the same code and maintaining separate copies becomes costly.