Skip to content

Commit

Permalink
Make return type Async.
Browse files Browse the repository at this point in the history
  • Loading branch information
nojaf committed Sep 15, 2023
1 parent 5ee6dcb commit 68f0a58
Show file tree
Hide file tree
Showing 8 changed files with 158 additions and 126 deletions.
106 changes: 54 additions & 52 deletions docs/content/Dual Analyzer.fsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,71 +24,73 @@ open FSharp.Compiler.Syntax
/// See https://learn.microsoft.com/en-us/dotnet/fsharp/style-guide/conventions#sort-open-statements-topologically
/// Note that this implementation is not complete and only serves as an illustration.
/// Nested modules are not taking into account.
let private topologicallySortedOpenStatementsAnalyzer (untypedTree: ParsedInput) : Message list =
let allOpenStatements =
let allOpenStatements = ResizeArray<string * range>()

let (|LongIdentAsString|) (lid: SynLongIdent) =
lid.LongIdent |> List.map (fun ident -> ident.idText) |> String.concat "."

let rec visitSynModuleSigDecl (decl: SynModuleSigDecl) =
match decl with
| SynModuleSigDecl.Open(SynOpenDeclTarget.ModuleOrNamespace(longId = LongIdentAsString value), mOpen) ->
allOpenStatements.Add(value, mOpen)
| _ -> ()

let rec visitSynModuleDecl (decl: SynModuleDecl) =
match decl with
| SynModuleDecl.Open(SynOpenDeclTarget.ModuleOrNamespace(longId = LongIdentAsString value), mOpen) ->
allOpenStatements.Add(value, mOpen)
| _ -> ()

match untypedTree with
| ParsedInput.SigFile(ParsedSigFileInput(contents = contents)) ->
for SynModuleOrNamespaceSig(decls = decls) in contents do
for decl in decls do
visitSynModuleSigDecl decl

| ParsedInput.ImplFile(ParsedImplFileInput(contents = contents)) ->
for SynModuleOrNamespace(decls = decls) in contents do
for decl in decls do
visitSynModuleDecl decl

allOpenStatements |> Seq.toList

let isOpenStatement (openStatement: string, _) = openStatement.StartsWith("System")

let nonSystemOpens = allOpenStatements |> List.skipWhile isOpenStatement

nonSystemOpens
|> List.filter isOpenStatement
|> List.map (fun (openStatement, mOpen) ->
{
Type = "Unsorted System open statement"
Message = $"%s{openStatement} was found after non System namespaces where opened!"
Code = "SOT001"
Severity = Warning
Range = mOpen
Fixes = []
}
)
let private topologicallySortedOpenStatementsAnalyzer (untypedTree: ParsedInput) : Async<Message list> =
async {
let allOpenStatements =
let allOpenStatements = ResizeArray<string * range>()

let (|LongIdentAsString|) (lid: SynLongIdent) =
lid.LongIdent |> List.map (fun ident -> ident.idText) |> String.concat "."

let rec visitSynModuleSigDecl (decl: SynModuleSigDecl) =
match decl with
| SynModuleSigDecl.Open(SynOpenDeclTarget.ModuleOrNamespace(longId = LongIdentAsString value), mOpen) ->
allOpenStatements.Add(value, mOpen)
| _ -> ()

let rec visitSynModuleDecl (decl: SynModuleDecl) =
match decl with
| SynModuleDecl.Open(SynOpenDeclTarget.ModuleOrNamespace(longId = LongIdentAsString value), mOpen) ->
allOpenStatements.Add(value, mOpen)
| _ -> ()

match untypedTree with
| ParsedInput.SigFile(ParsedSigFileInput(contents = contents)) ->
for SynModuleOrNamespaceSig(decls = decls) in contents do
for decl in decls do
visitSynModuleSigDecl decl

| ParsedInput.ImplFile(ParsedImplFileInput(contents = contents)) ->
for SynModuleOrNamespace(decls = decls) in contents do
for decl in decls do
visitSynModuleDecl decl

allOpenStatements |> Seq.toList

let isOpenStatement (openStatement: string, _) = openStatement.StartsWith("System")

let nonSystemOpens = allOpenStatements |> List.skipWhile isOpenStatement

return
nonSystemOpens
|> List.filter isOpenStatement
|> List.map (fun (openStatement, mOpen) ->
{
Type = "Unsorted System open statement"
Message = $"%s{openStatement} was found after non System namespaces where opened!"
Code = "SOT001"
Severity = Warning
Range = mOpen
Fixes = []
}
)
}

[<CliAnalyzer "Topologically sorted open statements">]
let cliAnalyzer (ctx: CliContext) =
let cliAnalyzer (ctx: CliContext) : Async<Message list> =
topologicallySortedOpenStatementsAnalyzer ctx.ParseFileResults.ParseTree

[<EditorAnalyzer "Topologically sorted open statements">]
let editorAnalyzer (ctx: EditorContext) =
let editorAnalyzer (ctx: EditorContext) : Async<Message list> =
match ctx.ParseFileResults with
// The editor might not have any parse results for a given file. So we don't return any messages.
| None -> []
| None -> async.Return []
| Some parseResults -> topologicallySortedOpenStatementsAnalyzer parseResults.ParseTree

(**
Both analyzer will follow the same code path: the console application will always have the required data, while the editor needs to be more careful.
⚠️ Please do not be tempted by calling `.Value` on the `EditorContext` 😉.
[Previous]({{fsdocs-previous-page-link}})
[Next]({{fsdocs-next-page-link}})
*)
29 changes: 16 additions & 13 deletions docs/content/Getting Started.fsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,19 +58,22 @@ module OptionAnalyzer =
[<CliAnalyzer>]
let optionValueAnalyzer: Analyzer<CliContext> =
fun (context: CliContext) ->
// inspect context to determine the error/warning messages
// A potential implementation might traverse the untyped syntax tree
// to find any references of `Option.Value`
[
{
Type = "Option.Value analyzer"
Message = "Option.Value shouldn't be used"
Code = "OV001"
Severity = Warning
Range = FSharp.Compiler.Text.Range.Zero
Fixes = []
}
]
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 = Warning
Range = FSharp.Compiler.Text.Range.Zero
Fixes = []
}
]
}

(**
## Running your first analyzer
Expand Down
39 changes: 21 additions & 18 deletions samples/OptionAnalyzer/Library.fs
Original file line number Diff line number Diff line change
Expand Up @@ -112,25 +112,28 @@ let notUsed () =
[<CliAnalyzer "OptionAnalyzer">]
let optionValueAnalyzer: Analyzer<CliContext> =
fun ctx ->
let state = ResizeArray<range>()
async {
let state = ResizeArray<range>()

let handler (range: range) (m: FSharpMemberOrFunctionOrValue) =
let name = String.Join(".", m.DeclaringEntity.Value.FullName, m.DisplayName)
let handler (range: range) (m: FSharpMemberOrFunctionOrValue) =
let name = String.Join(".", m.DeclaringEntity.Value.FullName, m.DisplayName)

if name = "Microsoft.FSharp.Core.FSharpOption`1.Value" then
state.Add range
if name = "Microsoft.FSharp.Core.FSharpOption`1.Value" then
state.Add range

ctx.TypedTree.Declarations |> List.iter (visitDeclaration handler)
ctx.TypedTree.Declarations |> List.iter (visitDeclaration handler)

state
|> Seq.map (fun r ->
{
Type = "Option.Value analyzer"
Message = "Option.Value shouldn't be used"
Code = "OV001"
Severity = Warning
Range = r
Fixes = []
}
)
|> Seq.toList
return
state
|> Seq.map (fun r ->
{
Type = "Option.Value analyzer"
Message = "Option.Value shouldn't be used"
Code = "OV001"
Severity = Warning
Range = r
Fixes = []
}
)
|> Seq.toList
}
22 changes: 15 additions & 7 deletions src/FSharp.Analyzers.Cli/Program.fs
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ let runProject (client: Client<CliAnalyzerAttribute, CliContext>) toolsPath proj
let! option = loadProject toolsPath path
let! checkProjectResults = fcs.ParseAndCheckProject(option)

return
let! messagesPerAnalyzer =
option.SourceFiles
|> Array.filter (fun file ->
match globs |> List.tryFind (fun g -> g.IsMatch file) with
Expand All @@ -118,21 +118,28 @@ let runProject (client: Client<CliAnalyzerAttribute, CliContext>) toolsPath proj
typeCheckFile option fileName sourceText
|> Option.map (createContext checkProjectResults fileName sourceText)
)
|> Array.collect (fun ctx ->
|> Array.map (fun ctx ->
match ctx with
| Some c ->
printInfo "Running analyzers for %s" c.FileName
client.RunAnalyzers c
| None -> failwithf "could not get context for file %s" path
)
|> Some
|> Async.Parallel

return
Some
[
for messages in messagesPerAnalyzer do
yield! messages
]
}

let printMessages failOnWarnings (msgs: Message array) =
let printMessages failOnWarnings (msgs: Message list) =
if verbose then
printfn ""

if verbose && Array.isEmpty msgs then
if verbose && List.isEmpty msgs then
printfn "No messages found from the analyzer(s)"

msgs
Expand All @@ -143,6 +150,7 @@ let printMessages failOnWarnings (msgs: Message array) =
| Warning when failOnWarnings |> List.contains m.Code -> ConsoleColor.Red
| Warning -> ConsoleColor.DarkYellow
| Info -> ConsoleColor.Blue
| Hint -> ConsoleColor.Cyan

Console.ForegroundColor <- color

Expand All @@ -160,13 +168,13 @@ let printMessages failOnWarnings (msgs: Message array) =

msgs

let calculateExitCode failOnWarnings (msgs: Message array option) : int =
let calculateExitCode failOnWarnings (msgs: Message list option) : int =
match msgs with
| None -> -1
| Some msgs ->
let check =
msgs
|> Array.exists (fun n ->
|> List.exists (fun n ->
n.Severity = Error
|| (n.Severity = Warning && failOnWarnings |> List.contains n.Code)
)
Expand Down
80 changes: 48 additions & 32 deletions src/FSharp.Analyzers.SDK/FSharp.Analyzers.SDK.Client.fs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ module Client =
Some(m.Invoke(null, null) |> unboxAnalyzer)
elif
m.ReturnType.FullName.StartsWith
"Microsoft.FSharp.Collections.FSharpList`1[[FSharp.Analyzers.SDK.Message"
"Microsoft.FSharp.Control.FSharpAsync`1[[Microsoft.FSharp.Collections.FSharpList`1[[FSharp.Analyzers.SDK.Message"
then
try
let analyzer: Analyzer<'TContext> = fun ctx -> m.Invoke(null, [| ctx |]) |> unbox
Expand Down Expand Up @@ -138,43 +138,59 @@ type Client<'TAttribute, 'TContext when 'TAttribute :> AnalyzerAttribute and 'TC
for path, analyzers in analyzers do
let analyzers = Seq.toList analyzers

if List.isEmpty analyzers then
failwith "no analyzers"

registeredAnalyzers.AddOrUpdate(path, analyzers, (fun _ _ -> analyzers))
|> ignore

if registeredAnalyzers.Count = 0 then
failwith "Nothing was added"

Seq.length analyzers, analyzers |> Seq.collect snd |> Seq.length
else
0, 0

member x.RunAnalyzers(ctx: 'TContext) : Message array =
let analyzers = registeredAnalyzers.Values |> Seq.collect id

analyzers
|> Seq.collect (fun (_analyzerName, analyzer) ->
try
analyzer ctx
with error ->
[]
)
|> Seq.toArray

member x.RunAnalyzersSafely(ctx: 'TContext) : AnalysisResult list =
let analyzers = registeredAnalyzers.Values |> Seq.collect id

analyzers
|> Seq.map (fun (analyzerName, analyzer) ->
{
AnalyzerName = analyzerName
Output =
member x.RunAnalyzers(ctx: 'TContext) : Async<Message list> =
async {
let analyzers = registeredAnalyzers.Values |> Seq.collect id

let! messagesPerAnalyzer =
analyzers
|> Seq.map (fun (_analyzerName, analyzer) ->
try
Ok(analyzer ctx)
analyzer ctx
with error ->
Result.Error error
}
)
|> Seq.toList
async.Return []
)
|> Async.Parallel

return
[
for messages in messagesPerAnalyzer do
yield! messages
]
}

member x.RunAnalyzersSafely(ctx: 'TContext) : Async<AnalysisResult list> =
async {
let analyzers = registeredAnalyzers.Values |> Seq.collect id

let! results =
analyzers
|> Seq.map (fun (analyzerName, analyzer) ->
async {
try
let! result = analyzer ctx

return
{
AnalyzerName = analyzerName
Output = Result.Ok result
}
with error ->
return
{
AnalyzerName = analyzerName
Output = Result.Error error
}
}
)
|> Async.Parallel

return List.ofArray results
}
4 changes: 2 additions & 2 deletions src/FSharp.Analyzers.SDK/FSharp.Analyzers.SDK.Client.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ type Client<'TAttribute, 'TContext when 'TAttribute :> AnalyzerAttribute and 'TC
member LoadAnalyzers: printError: (string -> unit) -> dir: string -> int * int
/// <summary>Runs all registered analyzers for given context (file).</summary>
/// <returns>list of messages. Ignores errors from the analyzers</returns>
member RunAnalyzers: ctx: 'TContext -> Message array
member RunAnalyzers: ctx: 'TContext -> Async<Message list>
/// <summary>Runs all registered analyzers for given context (file).</summary>
/// <returns>list of results per analyzer which can either messages or an exception.</returns>
member RunAnalyzersSafely: ctx: 'TContext -> AnalysisResult list
member RunAnalyzersSafely: ctx: 'TContext -> Async<AnalysisResult list>
2 changes: 1 addition & 1 deletion src/FSharp.Analyzers.SDK/FSharp.Analyzers.SDK.fs
Original file line number Diff line number Diff line change
Expand Up @@ -138,4 +138,4 @@ type Message =
Fixes: Fix list
}

type Analyzer<'TContext> = 'TContext -> Message list
type Analyzer<'TContext> = 'TContext -> Async<Message list>
2 changes: 1 addition & 1 deletion src/FSharp.Analyzers.SDK/FSharp.Analyzers.SDK.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -118,4 +118,4 @@ type Message =
Fixes: Fix list
}

type Analyzer<'TContext> = 'TContext -> Message list
type Analyzer<'TContext> = 'TContext -> Async<Message list>

0 comments on commit 68f0a58

Please sign in to comment.