Skip to content

Instantly share code, notes, and snippets.

@aggieben
Last active June 1, 2018 22:42
Show Gist options
  • Save aggieben/576fc049e6d17567ee905fed8858efab to your computer and use it in GitHub Desktop.
Save aggieben/576fc049e6d17567ee905fed8858efab to your computer and use it in GitHub Desktop.
Tool to find catch blocks for a specific exception type
module ExceptionHandlers
open System
open System.Collections.Generic
open System.Diagnostics
open Microsoft.Build.Locator
open Microsoft.CodeAnalysis
open Microsoft.CodeAnalysis.MSBuild
open Microsoft.CodeAnalysis.CSharp
open Microsoft.CodeAnalysis.Text
open Microsoft.CodeAnalysis.Workspaces
open System.Threading
open Microsoft.CodeAnalysis.CSharp.Syntax
let hasExceptionDeclaration exceptionType (ccSyntax:CatchClauseSyntax) =
if isNull ccSyntax.Declaration then false
else ccSyntax.Declaration.Type.ToString() = exceptionType
let hasExceptionFilter exceptionType (ccSyntax:CatchClauseSyntax) =
if isNull ccSyntax.Filter then false
else ccSyntax.Filter.DescendantNodesAndSelf()
|> Seq.exists (fun node -> node :? TypeSyntax && (node :?> TypeSyntax).ToString() = exceptionType)
let usesExceptionInBlock exceptionType (ccSyntax:CatchClauseSyntax) =
let isTypeSyntaxOfType (t:string) (node:SyntaxNode) = node :? TypeSyntax && (node :?> TypeSyntax).ToString() = t
let isInNewObjectExpression (node:TypeSyntax) = node.Ancestors() |> Seq.exists (fun sn -> sn :? ObjectCreationExpressionSyntax)
let allNodes = ccSyntax.Block.DescendantNodesAndSelf()
let typeNodes = Seq.filter (isTypeSyntaxOfType exceptionType) allNodes
|> Seq.map (fun node -> node :?> TypeSyntax)
Seq.exists (fun node -> isTypeSyntaxOfType exceptionType node
&& (not << isInNewObjectExpression) node) typeNodes
let isExceptionCatchClause exceptionType (ccSyntax:CatchClauseSyntax) =
hasExceptionDeclaration exceptionType ccSyntax
|| hasExceptionFilter exceptionType ccSyntax
|| usesExceptionInBlock exceptionType ccSyntax
let analyzeHandlers exceptionType (project:Project) = async {
let! compilation = project.GetCompilationAsync() |> Async.AwaitTask
let catchClauses = compilation.SyntaxTrees
|> Seq.collect (fun stree -> stree.GetRoot(CancellationToken.None).DescendantNodesAndSelf()
|> Seq.filter (fun node -> node :? CatchClauseSyntax))
|> Seq.map (fun node -> node :?> CatchClauseSyntax)
|> Seq.filter (isExceptionCatchClause exceptionType)
for cc in catchClauses do
let flps = cc.GetLocation().GetLineSpan()
// let line = (cc.GetText()).Lines.[0]
printfn "found catch clause: %s:%d" flps.Path flps.StartLinePosition.Line
printfn "%s" (cc.ToString())
return Seq.length catchClauses
}
[<EntryPoint>]
let main argv =
MSBuildLocator.RegisterDefaults() |> ignore
use workspace = MSBuildWorkspace.Create()
let solution = workspace.OpenSolutionAsync(argv.[0]) |> Async.AwaitTask |> Async.RunSynchronously
printfn "Opened %d projects." (Seq.length solution.Projects)
let nonTestProjects = solution.Projects
|> Seq.filter (fun p -> p.Name.Contains("Test") |> not)
printfn "Found %d non-test projects." (Seq.length nonTestProjects)
let appProjects = nonTestProjects |> Project.getAppProjects
printfn "Found %d app projects" (Seq.length appProjects)
let numCatchClauses = appProjects
|> Seq.map (analyzeHandlers argv.[1])
|> Async.Parallel
|> Async.RunSynchronously
|> Array.sum
printfn "Found %d catch clauses" numCatchClauses
0 // return an integer exit code
module Project
open System
open Microsoft.CodeAnalysis
type MSBuildProjectProperty = Microsoft.Build.Evaluation.ProjectProperty
type MSBuildProject = Microsoft.Build.Evaluation.Project
let getReferencedProjectIds (proj:Project) =
match List.ofSeq proj.ProjectReferences with
| [] -> []
| prl -> List.map (fun (pr:ProjectReference) -> pr.ProjectId.Id) prl
let appProjectTypes = set [Guid.Parse("{349c5851-65df-11da-9384-00065b846f21}"); // web application
Guid.Parse("{E3E379DF-F4C6-4180-9B81-6769533ABE47}"); // mvc 4
]
let hasProjectType (guids:Set<Guid>) (msbproj:MSBuildProject) =
match Seq.tryFind (fun (prop:MSBuildProjectProperty) -> prop.Name = "ProjectTypeGuids") msbproj.Properties with
| None -> false
| Some prop -> prop.EvaluatedValue.Split([|';'|], StringSplitOptions.RemoveEmptyEntries)
|> Array.map (fun s -> Guid.Parse(s)) |> set
|> Set.intersect guids
|> Set.isEmpty |> not
let hasExeOutputType (msbproj:MSBuildProject) =
match Seq.tryFind (fun (prop:MSBuildProjectProperty) -> prop.Name = "OutputType") msbproj.Properties with
| None -> false
| Some prop ->
let value = prop.EvaluatedValue.ToLower()
value = "exe" || value = "winexe"
let getAppProjects (projects:Project seq) =
let projectMap = projects |> Seq.fold (fun map p -> Map.add p.Id.Id (MSBuildProject(p.FilePath)) map) Map.empty
let projectIds = projects
|> Seq.filter (fun p -> (p.Name.ToLower()).Contains("test") |> not)
|> Seq.map (fun p -> projectMap.[p.Id.Id])
|> Seq.filter (fun mp -> (hasExeOutputType mp) || (hasProjectType appProjectTypes mp))
|> Seq.map (fun mp -> Map.pick (fun key msproj -> if msproj = mp then Some key else None) projectMap)
projects |> Seq.filter (fun p -> Seq.contains p.Id.Id projectIds)
let getRootProjects (projects:Project seq) =
let referencedProjects = projects
|> Seq.map getReferencedProjectIds
|> Seq.fold (fun state pidl -> Set.union (Set.ofSeq pidl) state) Set.empty
projects |> Seq.filter (fun p -> Set.contains p.Id.Id referencedProjects |> not)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment