Logo FSharp.Analyzers.SDK

Getting started writing an analyzer

Premise

Analyzers that are consumed by this SDK and from Ionide are simply .NET core class libraries. These class libraries expose a value of type Analyzer<'TContext> which is effectively a function that has input of type Context and returns a list of Message records.

Create project

Create a new class library targeting net10.0

dotnet new classlib -lang F# -f net10.0 -n OptionValueAnalyzer

Note that the assembly name needs to contain Analyzer in the name in order for it to be picked up.

Add a reference to the analyzers SDK:

dotnet add package FSharp.Analyzers.SDK

⚠️ Note: To utilize the analyzers in FsAutoComplete (which is subsequently utilized by Ionide), it is essential to ensure that the SDK version matches correctly.

paket add FSharp.Analyzers.SDK

The FSharp.Analyzers.SDK takes a dependency on FSharp.Compiler.Service, which has a strict dependency on FSharp.Core. It is considered a best practice to use the correct FSharp.Core version and not the implicit one from the SDK.

<PackageReference Update="FSharp.Core" Version="10.1.402" />

First analyzer

An Analyzer<'TContext> is a function that takes a Context and returns a list of Message. There are two flavours of analyzers:

The key difference between them is that the console application analyzer will have the full project information. Per file this includes the untyped tree, typed tree, type-check results of the file and project type-check results. The fsharp-analyzers tool will collect all this information upfront and pass it down to the analyzer via the CliContext.

In the case of an editor analyzer, the IDE might not have all the available information available and will be more selective in what it can pass down to the analyzer. The main reasoning behind this is performance. It might be desirable for some analyzers to run after every keystroke, while others should be executed more sparingly.

In the following example we will be

open FSharp.Analyzers.SDK

// This attribute is required and needs to match the correct context type!
[<CliAnalyzer>]
let optionValueAnalyzer: Analyzer<CliContext> =
    fun (context: CliContext) ->
        async {
            // inspect context to determine the error/warning messages
            // A potential implementation might traverse the untyped syntax tree
            // to find any references of `Option.Value`
            return
                [
                    {
                        Type = "Option.Value analyzer"
                        Message = "Option.Value shouldn't be used"
                        Code = "OV001"
                        Severity = Severity.Warning
                        Range = FSharp.Compiler.Text.Range.range0
                        Fixes = []
                    }
                ]
        }

Analyzers can also be named which allows for better logging if something went wrong while using the SDK from Ionide:

[<EditorAnalyzer "BadCodeAnalyzer">]
let badCodeAnalyzer: Analyzer<EditorContext> =
    fun (context: EditorContext) ->
        async { // inspect context to determine the error/warning messages
            return []
        }

Running your first analyzer

After building your project you can run your analyzer on a project of your choosing using the fsharp-analyzers tool. Again, please verify your analyzer is a CliAnalyzerAttribute and uses the CliContext!

dotnet tool install --global fsharp-analyzers
fsharp-analyzers --project YourProject.fsproj --analyzers-path ./OptionAnalyzer/bin/Release --verbosity d

Packaging and Distribution

Since analyzers are just .NET core libraries, you can distribute them to the nuget registry just like you would with a normal .NET package. Simply run dotnet pack --configuration Release against the analyzer project to get a nuget package and publish it with

dotnet nuget push {NugetPackageFullPath} -s nuget.org -k {NugetApiKey}

However, the story is different and slightly more complicated when your analyzer package has third-party dependencies also coming from nuget. Since the SDK dynamically loads the package assemblies (.dll files), the assemblies of the dependencies have to be right next to the main assembly of the analyzer. Using dotnet pack will not include these dependencies into the output Nuget package. More specifically, the ./lib/net10.0 directory of the nuget package must have all the required assemblies, also those from third-party packages. In order to package the analyzer properly with all the assemblies, you need to take the output you get from running:

dotnet publish --configuration Release --framework net10.0

against the analyzer project and put every file from that output into the ./lib/net10.0 directory of the nuget package. This requires some manual work by unzipping the nuget package first (because it is just an archive), modifying the directories then zipping the package again. It can be done using a FAKE build target to automate the work:

// make ZipFile available
#r "System.IO.Compression.FileSystem.dll"
#r "nuget: Fake.Core.Target, 6.0.0"
#r "nuget: Fake.Core.ReleaseNotes, 6.0.0"
#r "nuget: Fake.IO.Zip, 6.0.0"

open System.IO
open System.IO.Compression
open Fake.Core
open Fake.IO
open Fake.IO.FileSystemOperators

let releaseNotes = ReleaseNotes.load "RELEASE_NOTES.md"

Target.create
    "PackAnalyzer"
    (fun _ ->
        let analyzerProject = "src" </> "BadCodeAnalyzer"

        let args =
            [
                "pack"
                "--configuration Release"
                sprintf "/p:PackageVersion=%s" releaseNotes.NugetVersion
                sprintf "/p:PackageReleaseNotes=\"%s\"" (String.concat "\n" releaseNotes.Notes)
                sprintf
                    "--output %s"
                    (__SOURCE_DIRECTORY__
                     </> "dist")
            ]

        // create initial nuget package
        let exitCode = Shell.Exec("dotnet", String.concat " " args, analyzerProject)

        if exitCode <> 0 then
            failwith "dotnet pack failed"
        else
            match
                Shell.Exec(
                    "dotnet",
                    "publish --configuration Release --framework net10.0",
                    analyzerProject
                )
            with
            | 0 ->
                let nupkg =
                    System.IO.Directory.GetFiles(
                        __SOURCE_DIRECTORY__
                        </> "dist"
                    )
                    |> Seq.head
                    |> Path.GetFullPath

                let nugetParent = DirectoryInfo(nupkg).Parent.FullName
                let nugetFileName = Path.GetFileNameWithoutExtension(nupkg)

                let publishPath =
                    analyzerProject
                    </> "bin"
                    </> "Release"
                    </> "net10.0"
                    </> "publish"
                // Unzip the nuget
                ZipFile.ExtractToDirectory(nupkg, nugetParent </> nugetFileName)
                // delete the initial nuget package
                File.Delete nupkg
                // remove stuff from ./lib/net10.0
                Shell.deleteDir (
                    nugetParent
                    </> nugetFileName
                    </> "lib"
                    </> "net10.0"
                )
                // move the output of publish folder into the ./lib/net10.0 directory
                Shell.copyDir
                    (nugetParent
                     </> nugetFileName
                     </> "lib"
                     </> "net10.0")
                    publishPath
                    (fun _ -> true)
                // re-create the nuget package
                ZipFile.CreateFromDirectory(nugetParent </> nugetFileName, nupkg)
                // delete intermediate directory
                Shell.deleteDir (nugetParent </> nugetFileName)
            | _ -> failwith "dotnet publish failed"
    )

Package metadata for consumers

Two pieces of metadata make life much easier for people (and tools) consuming your analyzer package.

Tag the package with fsharp-analyzer. This is how analyzer packages are found on NuGet, both by humans (search by tag) and by tooling that needs to tell analyzer packages apart from ordinary package references.

State the SDK version in your release notes. The fsharp-analyzers tool only loads analyzers built against the same major.minor of FSharp.Analyzers.SDK as the tool itself, and skips the rest with an error in the log. Your package does not declare the SDK as a NuGet dependency, so the release notes are the only place a consumer can see which SDK a given release targets without downloading and inspecting the assembly. Every time you bump the SDK, add a line such as Update FSharp.Analyzers.SDK to 0.38.0 to your changelog and make sure it ends up in the package release notes. Ionide.Analyzers and G-Research.FSharp.Analyzers follow this convention.

<PropertyGroup>
    <PackageTags>F#, fsharp, analyzers, fsharp-analyzer</PackageTags>
    <!-- Ionide.KeepAChangelog.Tasks fills PackageReleaseNotes from CHANGELOG.md automatically -->
    <PackageReleaseNotes>https://github.com/you/your-analyzers/blob/main/CHANGELOG.md</PackageReleaseNotes>
</PropertyGroup>

Consumers upgrading with the upgrade-fsharp-analyzers agent skill rely on both of these.

Known footguns to avoid

There's a footgun in the FCS-API that you can easily trigger when working on an analyzer: Accessing the FullName property of the FSharpEntity type throws an exception if the entity doesn't have one. Use the TryGetFullName function for safe access.

Previous Next

Multiple items
namespace FSharp

--------------------
namespace Microsoft.FSharp
namespace FSharp.Analyzers
namespace FSharp.Analyzers.SDK
Multiple items
Marks an analyzer for scanning during the console application run.
type CliAnalyzerAttribute =
  inherit AnalyzerAttribute
  new:
      [<Optional; DefaultParameterValue (("Analyzer" :> obj))>] name: string *
      [<Optional; DefaultParameterValue (("" :> obj))>] shortDescription: string *
      [<Optional; DefaultParameterValue (("" :> obj))>] helpUri: string ->
          CliAnalyzerAttribute
  member Name: string

--------------------
new:
    [<System.Runtime.InteropServices.Optional; System.Runtime.InteropServices.DefaultParameterValue (("Analyzer" :> obj))>] name: string *
    [<System.Runtime.InteropServices.Optional; System.Runtime.InteropServices.DefaultParameterValue (("" :> obj))>] shortDescription: string *
    [<System.Runtime.InteropServices.Optional; System.Runtime.InteropServices.DefaultParameterValue (("" :> obj))>] helpUri: string ->
        CliAnalyzerAttribute
val optionValueAnalyzer: context: CliContext -> Async<Message list>
Multiple items
type Analyzer<'TContext> = 'TContext -> Async<Message list>

--------------------
type AnalyzerAttribute =
  inherit Attribute
  new:
      [<Optional; DefaultParameterValue (("Analyzer" :> obj))>] name: string *
      [<Optional; DefaultParameterValue (("" :> obj))>] shortDescription: string *
      [<Optional; DefaultParameterValue (("" :> obj))>] helpUri: string ->
          AnalyzerAttribute
  member HelpUri: string option
  member Name: string
  member ShortDescription: string option

--------------------
new:
    [<System.Runtime.InteropServices.Optional; System.Runtime.InteropServices.DefaultParameterValue (("Analyzer" :> obj))>] name: string *
    [<System.Runtime.InteropServices.Optional; System.Runtime.InteropServices.DefaultParameterValue (("" :> obj))>] shortDescription: string *
    [<System.Runtime.InteropServices.Optional; System.Runtime.InteropServices.DefaultParameterValue (("" :> obj))>] helpUri: string ->
        AnalyzerAttribute
All the relevant compiler information for a given file.
Contains the source text, untyped and typed tree information.

type CliContext =
  {
    FileName: string
    SourceText: ISourceText
    ParseFileResults: FSharpParseFileResults
    CheckFileResults: FSharpCheckFileResults
    TypedTree: FSharpImplementationFileContents option
    CheckProjectResults: FSharpCheckProjectResults
    ProjectOptions: AnalyzerProjectOptions
    AnalyzerIgnoreRanges: Map<string,AnalyzerIgnoreRange list>
  }
  interface Context
  member GetAllEntities: publicOnly: bool -> AssemblySymbol list
  member GetAllSymbolUsesOfFile: unit -> FSharpSymbolUse seq
  member GetAllSymbolUsesOfProject: unit -> FSharpSymbolUse array
val context: CliContext
val async: AsyncBuilder
type Message =
  {
    Type: string
    Message: string
    Code: string
    Severity: Severity
    Range: range
    Fixes: Fix list
  }
type Severity =
  | Info
  | Hint
  | Warning
  | Error
union case Severity.Warning: Severity
union case AnalyzerIgnoreRange.Range:
    commentStart: int *
    commentEnd: int ->
        AnalyzerIgnoreRange
namespace FSharp.Compiler
namespace FSharp.Compiler.Text
Multiple items
module Range
from FSharp.Compiler.Text

--------------------
type Range =
  member ApplyLineDirectives: unit -> range
  member End: pos
  member EndColumn: int
  member EndLine: int
  member EndRange: range
  member FileName: string
  member IsSynthetic: bool
  member Start: pos
  member StartColumn: int
  member StartLine: int
  ...
val range0: FSharp.Compiler.Text.range
Multiple items
Marks an analyzer for scanning during IDE integration.
type EditorAnalyzerAttribute =
  inherit AnalyzerAttribute
  new:
      [<Optional; DefaultParameterValue (("Analyzer" :> obj))>] name: string *
      [<Optional; DefaultParameterValue (("" :> obj))>] shortDescription: string *
      [<Optional; DefaultParameterValue (("" :> obj))>] helpUri: string ->
          EditorAnalyzerAttribute
  member Name: string

--------------------
new:
    [<System.Runtime.InteropServices.Optional; System.Runtime.InteropServices.DefaultParameterValue (("Analyzer" :> obj))>] name: string *
    [<System.Runtime.InteropServices.Optional; System.Runtime.InteropServices.DefaultParameterValue (("" :> obj))>] shortDescription: string *
    [<System.Runtime.InteropServices.Optional; System.Runtime.InteropServices.DefaultParameterValue (("" :> obj))>] helpUri: string ->
        EditorAnalyzerAttribute
val badCodeAnalyzer: context: EditorContext -> Async<Message list>
Optional compiler information for a given file.
The available contents is controlled based on what information the IDE has available.

type EditorContext =
  {
    FileName: string
    SourceText: ISourceText
    ParseFileResults: FSharpParseFileResults
    CheckFileResults: FSharpCheckFileResults option
    TypedTree: FSharpImplementationFileContents option
    CheckProjectResults: FSharpCheckProjectResults option
    ProjectOptions: AnalyzerProjectOptions
    AnalyzerIgnoreRanges: Map<string,AnalyzerIgnoreRange list>
  }
  interface Context
  member GetAllEntities: publicOnly: bool -> AssemblySymbol list
  member GetAllSymbolUsesOfFile: unit -> FSharpSymbolUse seq
  member GetAllSymbolUsesOfProject: unit -> FSharpSymbolUse array
val context: EditorContext
namespace System
namespace System.IO
namespace System.IO.Compression
namespace Fake
namespace Fake.Core
namespace Fake.IO
Defines custom operators for manipulating files and directories in a file system
module FileSystemOperators
from Fake.IO
val releaseNotes: ReleaseNotes.ReleaseNotes
Contains helpers which allow to parse Release Notes text files. Either "simple" or "complex" format is accepted.

Formats:
- Simple format

1.1.10 - Support for heterogeneous XML attributes. Make CsvFile re-entrant.
1.1.9 - Infer booleans for ints that only manifest 0 and 1.

- Complex format

### New in 1.1.10 (Released 2013/09/12)
    * Support for heterogeneous XML attributes.
    * Make CsvFile re-entrant.
    * Support for compressed HTTP responses.
    * Fix JSON conversion of 0 and 1 to booleans.

    ### New in 1.1.9 (Released 2013/07/21)
    * Infer booleans for ints that only manifest 0 and 1.
    * Support for partially overriding the Schema in CsvProvider.
    * PreferOptionals and SafeMode parameters for CsvProvider.


#r "paket:
    nuget Fake.Core.ReleaseNotes //"

    let release = ReleaseNotes.load "RELEASE_NOTES.md"

    Target "AssemblyInfo" (fun _ ->
        CreateFSharpAssemblyInfo "src/Common/AssemblyInfo.fs"
          [ Attribute.Title project
            Attribute.Product project
            Attribute.Description summary
            Attribute.Version release.AssemblyVersion
            Attribute.FileVersion release.AssemblyVersion]
    )

module ReleaseNotes
from Fake.Core
Parses a Release Notes text file and returns the latest release notes.

fileName: Release notes text file name

val load: fileName: string -> ReleaseNotes.ReleaseNotes
Multiple items
Core namespace contains FAKE's core tasks, like Target, Process, BuildeServer, etc.. modules.


FAKE Target module contains tasks to define and run targets.

module Target
from Fake.Core

--------------------
[omit]
type Target =
  {
    Name: string
    Dependencies: string list
    SoftDependencies: string list
    Description: TargetDescription option
    Function: (TargetParameter -> unit)
  }
  member DescriptionAsString: TargetDescription
Creates a Target.
val create: name: string -> body: (TargetParameter -> unit) -> unit
val analyzerProject: string
val args: string list
val sprintf: format: Printf.StringFormat<'T> -> 'T
The nuget package version
ReleaseNotes.ReleaseNotes.NugetVersion: string
Multiple items
Contains basic functions for string manipulation.
module String
from Fake.Core

--------------------
module String
from Microsoft.FSharp.Core
val concat: sep: string -> strings: string seq -> string
ReleaseNotes.ReleaseNotes.Notes: string list
val exitCode: int
Multiple items
Shell-like functions. Similar to
Ruby's FileUtils.

module Shell
from Fake.IO

--------------------
Allows to exec shell operations synchronously and asynchronously.
type Shell =
  static member AsyncExec: cmd: string * ?args: string * ?dir: string -> int
  static member Exec: cmd: string * ?args: string * ?dir: string -> int
static member Shell.Exec: cmd: string * ?args: string * ?dir: string -> int
val failwith: message: string -> 'T
val nupkg: string
Exposes static methods for creating, moving, and enumerating through directories and subdirectories. This class cannot be inherited.
type Directory =
  static member CreateDirectory: path: string -> DirectoryInfo + 1 overload
  static member CreateSymbolicLink:
      path: string *
      pathToTarget: string ->
          FileSystemInfo
  static member CreateTempSubdirectory: ?prefix: string -> DirectoryInfo
  static member Delete: path: string -> unit + 1 overload
  static member EnumerateDirectories: path: string -> string seq + 3 overloads
  static member EnumerateFileSystemEntries: path: string -> string seq + 3 overloads
  static member EnumerateFiles: path: string -> string seq + 3 overloads
  static member Exists: path: string -> bool
  static member GetCreationTime: path: string -> DateTime
  static member GetCreationTimeUtc: path: string -> DateTime
  ...
Directory.GetFiles(path: string) : string array
Directory.GetFiles(path: string, searchPattern: string) : string array
Directory.GetFiles(path: string, searchPattern: string, searchOption: SearchOption) : string array
Directory.GetFiles(path: string, searchPattern: string, enumerationOptions: EnumerationOptions) : string array
module Seq
from Microsoft.FSharp.Collections
val head: source: 'T seq -> 'T
Performs operations on String instances that contain file or directory path information. These operations are performed in a cross-platform manner.
type Path =
  static member ChangeExtension: path: string * extension: string -> string
  static member Combine: path1: string * path2: string -> string + 4 overloads
  static member EndsInDirectorySeparator: path: ReadOnlySpan<char> -> bool + 1 overload
  static member Exists: path: string -> bool
  static member GetDirectoryName: path: ReadOnlySpan<char> -> ReadOnlySpan<char> + 1 overload
  static member GetExtension: path: ReadOnlySpan<char> -> ReadOnlySpan<char> + 1 overload
  static member GetFileName: path: ReadOnlySpan<char> -> ReadOnlySpan<char> + 1 overload
  static member GetFileNameWithoutExtension: path: ReadOnlySpan<char> -> ReadOnlySpan<char> + 1 overload
  static member GetFullPath: path: string -> string + 1 overload
  static member GetInvalidFileNameChars: unit -> char array
  ...
Path.GetFullPath(path: string) : string
Path.GetFullPath(path: string, basePath: string) : string
val nugetParent: string
Multiple items
Exposes instance methods for creating, moving, and enumerating through directories and subdirectories. This class cannot be inherited.
type DirectoryInfo =
  inherit FileSystemInfo
  new: path: string -> unit
  member Create: unit -> unit
  member CreateSubdirectory: path: string -> DirectoryInfo
  member Delete: unit -> unit + 1 overload
  member EnumerateDirectories: unit -> DirectoryInfo seq + 3 overloads
  member EnumerateFileSystemInfos: unit -> FileSystemInfo seq + 3 overloads
  member EnumerateFiles: unit -> FileInfo seq + 3 overloads
  member GetDirectories: unit -> DirectoryInfo array + 3 overloads
  member GetFileSystemInfos: unit -> FileSystemInfo array + 3 overloads
  ...

--------------------
DirectoryInfo(path: string) : DirectoryInfo
val nugetFileName: string
Path.GetFileNameWithoutExtension(path: string) : string
Path.GetFileNameWithoutExtension(path: System.ReadOnlySpan<char>) : System.ReadOnlySpan<char>
val publishPath: string
Provides static methods for creating, extracting, and opening zip archives.
type ZipFile =
  static member CreateFromDirectory: sourceDirectoryName: string * destination: Stream -> unit + 5 overloads
  static member CreateFromDirectoryAsync: sourceDirectoryName: string * destination: Stream * compressionLevel: CompressionLevel * includeBaseDirectory: bool * entryNameEncoding: Encoding * ?cancellationToken: CancellationToken -> Task + 5 overloads
  static member ExtractToDirectory: source: Stream * destinationDirectoryName: string -> unit + 7 overloads
  static member ExtractToDirectoryAsync: source: Stream * destinationDirectoryName: string * overwriteFiles: bool * ?cancellationToken: CancellationToken -> Task + 7 overloads
  static member Open: archiveFileName: string * mode: ZipArchiveMode -> ZipArchive + 1 overload
  static member OpenAsync: archiveFileName: string * mode: ZipArchiveMode * entryNameEncoding: Encoding * ?cancellationToken: CancellationToken -> Task<ZipArchive> + 1 overload
  static member OpenRead: archiveFileName: string -> ZipArchive
  static member OpenReadAsync:
      archiveFileName: string *
      ?cancellationToken: CancellationToken ->
          Task<ZipArchive>
ZipFile.ExtractToDirectory(sourceArchiveFileName: string, destinationDirectoryName: string) : unit
ZipFile.ExtractToDirectory(source: Stream, destinationDirectoryName: string) : unit
ZipFile.ExtractToDirectory(sourceArchiveFileName: string, destinationDirectoryName: string, entryNameEncoding: System.Text.Encoding) : unit
ZipFile.ExtractToDirectory(sourceArchiveFileName: string, destinationDirectoryName: string, overwriteFiles: bool) : unit
ZipFile.ExtractToDirectory(source: Stream, destinationDirectoryName: string, entryNameEncoding: System.Text.Encoding) : unit
ZipFile.ExtractToDirectory(source: Stream, destinationDirectoryName: string, overwriteFiles: bool) : unit
ZipFile.ExtractToDirectory(sourceArchiveFileName: string, destinationDirectoryName: string, entryNameEncoding: System.Text.Encoding, overwriteFiles: bool) : unit
ZipFile.ExtractToDirectory(source: Stream, destinationDirectoryName: string, entryNameEncoding: System.Text.Encoding, overwriteFiles: bool) : unit
Provides static methods for the creation, copying, deletion, moving, and opening of a single file, and aids in the creation of FileStream objects.
type File =
  static member AppendAllBytes: path: string * bytes: byte array -> unit + 1 overload
  static member AppendAllBytesAsync: path: string * bytes: byte array * ?cancellationToken: CancellationToken -> Task + 1 overload
  static member AppendAllLines: path: string * contents: string seq -> unit + 1 overload
  static member AppendAllLinesAsync: path: string * contents: string seq * encoding: Encoding * ?cancellationToken: CancellationToken -> Task + 1 overload
  static member AppendAllText: path: string * contents: ReadOnlySpan<char> -> unit + 3 overloads
  static member AppendAllTextAsync: path: string * contents: ReadOnlyMemory<char> * encoding: Encoding * ?cancellationToken: CancellationToken -> Task + 3 overloads
  static member AppendText: path: string -> StreamWriter
  static member Copy: sourceFileName: string * destFileName: string -> unit + 1 overload
  static member Create: path: string -> FileStream + 2 overloads
  static member CreateSymbolicLink:
      path: string *
      pathToTarget: string ->
          FileSystemInfo
  ...
File.Delete(path: string) : unit
Delete a directory

dir: The directory path to delete

val deleteDir: dir: string -> unit
Copies a directory recursively. If the target directory does not exist, it will be created

target: The target directory
source: The source directory
filterFile: A file filter predicate

val copyDir:
    target: string ->
    source: string ->
    filterFile: (string -> bool) ->
        unit
ZipFile.CreateFromDirectory(sourceDirectoryName: string, destinationArchiveFileName: string) : unit
ZipFile.CreateFromDirectory(sourceDirectoryName: string, destination: Stream) : unit
ZipFile.CreateFromDirectory(sourceDirectoryName: string, destinationArchiveFileName: string, compressionLevel: CompressionLevel, includeBaseDirectory: bool) : unit
ZipFile.CreateFromDirectory(sourceDirectoryName: string, destination: Stream, compressionLevel: CompressionLevel, includeBaseDirectory: bool) : unit
ZipFile.CreateFromDirectory(sourceDirectoryName: string, destinationArchiveFileName: string, compressionLevel: CompressionLevel, includeBaseDirectory: bool, entryNameEncoding: System.Text.Encoding) : unit
ZipFile.CreateFromDirectory(sourceDirectoryName: string, destination: Stream, compressionLevel: CompressionLevel, includeBaseDirectory: bool, entryNameEncoding: System.Text.Encoding) : unit

Type something to start searching.