Skip to content

Commit

Permalink
Merge pull request #140 from dawedawe/treecollecting
Browse files Browse the repository at this point in the history
Treecollecting
  • Loading branch information
dawedawe authored Nov 3, 2023
2 parents 7605eaf + d96eeff commit 6a1815a
Show file tree
Hide file tree
Showing 8 changed files with 941 additions and 138 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres
to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
## [0.18.0] - 2023-11-03

### Added
* [Allow remapping of all severity levels](https://github.com/ionide/FSharp.Analyzers.SDK/pull/138) (thanks @dawedawe!)
* [Add tree processing infrastructure](https://github.com/ionide/FSharp.Analyzers.SDK/pull/140) (thanks @dawedawe!)

## [0.17.1] - 2023-10-30

Expand Down
56 changes: 32 additions & 24 deletions docs/content/Dual Analyzer.fsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ With a little orchestration it is possible to easily write two analyzer function
(** *)

open FSharp.Analyzers.SDK
open FSharp.Analyzers.SDK.ASTCollecting
open FSharp.Compiler.CodeAnalysis
open FSharp.Compiler.Text
open FSharp.Compiler.Syntax
Expand All @@ -38,30 +39,24 @@ let private topologicallySortedOpenStatementsAnalyzer
let (|LongIdentAsString|) (lid: SynLongIdent) =
lid.LongIdent |> List.map (fun ident -> ident.idText)

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

let rec visitSynModuleDecl (decl: SynModuleDecl) =
match decl with
| SynModuleDecl.Open(
target = SynOpenDeclTarget.ModuleOrNamespace(longId = LongIdentAsString value; range = 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
let walker =
{ new SyntaxCollectorBase() with
override _.WalkSynModuleSigDecl(decl: SynModuleSigDecl) =
match decl with
| SynModuleSigDecl.Open(
target = SynOpenDeclTarget.ModuleOrNamespace(longId = LongIdentAsString value; range = mOpen)) ->
allOpenStatements.Add(value, mOpen)
| _ -> ()

override _.WalkSynModuleDecl(decl: SynModuleDecl) =
match decl with
| SynModuleDecl.Open(
target = SynOpenDeclTarget.ModuleOrNamespace(longId = LongIdentAsString value; range = mOpen)) ->
allOpenStatements.Add(value, mOpen)
| _ -> ()
}

ASTCollecting.walkAst walker untypedTree

allOpenStatements |> Seq.toList

Expand Down Expand Up @@ -115,6 +110,19 @@ let editorAnalyzer (ctx: EditorContext) : Async<Message list> =
Both analyzers 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` 😉.
To enable a wide range of analyzers, both context types give access to very detailed information about the source code.
Among this information is the full untyped abstract syntax tree (AST) and the typed abstract syntax tree (TAST).
As you can deduce from the example above, processing these trees is a very common task in an analyzer. But writing your own tree traversal code can be daunting and can also get quite repetitive over many analyzers.
That's why the SDK offers the `ASTCollecting` and `TASTCollecting` modules. In there, you'll find facility types and functions to make your analyzers author life easier.
For both trees, a type is defined, [SyntaxCollectorBase](../reference/fsharp-analyzers-sdk-astcollecting-syntaxcollectorbase.html) and [TypedTreeCollectorBase](../reference/fsharp-analyzers-sdk-tastcollecting-typedtreecollectorbase.html) respectively,
with members you can override to have easy access to the tree elements you want to process.
Just pass an instance with your overriden members to the `walkAst` or `walkTast` function.
The open-statement analyzer from above uses the AST for it's analysis.
Because we want to process the `SynModuleSigDecl` and `SynModuleDecl` elements of the AST, we just override the two appropriate members of the `SyntaxCollectorBase` type
in an [object expression](https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/object-expressions) and pass the instance to `walkAst`.
Much simpler and shorter than doing the traversal ourselves.
[Previous]({{fsdocs-previous-page-link}})
[Next]({{fsdocs-next-page-link}})
*)
116 changes: 10 additions & 106 deletions samples/OptionAnalyzer/Library.fs
Original file line number Diff line number Diff line change
Expand Up @@ -2,109 +2,9 @@

open System
open FSharp.Analyzers.SDK
open FSharp.Compiler.Symbols
open FSharp.Compiler.Symbols.FSharpExprPatterns
open FSharp.Analyzers.SDK.TASTCollecting
open FSharp.Compiler.Text

let rec visitExpr memberCallHandler (e: FSharpExpr) =
match e with
| AddressOf(lvalueExpr) -> visitExpr memberCallHandler lvalueExpr
| AddressSet(lvalueExpr, rvalueExpr) ->
visitExpr memberCallHandler lvalueExpr
visitExpr memberCallHandler rvalueExpr
| Application(funcExpr, typeArgs, argExprs) ->
visitExpr memberCallHandler funcExpr
visitExprs memberCallHandler argExprs
| Call(objExprOpt, memberOrFunc, typeArgs1, typeArgs2, argExprs) ->
memberCallHandler e.Range memberOrFunc
visitObjArg memberCallHandler objExprOpt
visitExprs memberCallHandler argExprs
| Coerce(targetType, inpExpr) -> visitExpr memberCallHandler inpExpr
| FastIntegerForLoop(startExpr, limitExpr, consumeExpr, isUp, debugPointAtFor, debugPointAtInOrTo) ->
visitExpr memberCallHandler startExpr
visitExpr memberCallHandler limitExpr
visitExpr memberCallHandler consumeExpr
| ILAsm(asmCode, typeArgs, argExprs) -> visitExprs memberCallHandler argExprs
| ILFieldGet(objExprOpt, fieldType, fieldName) -> visitObjArg memberCallHandler objExprOpt
| ILFieldSet(objExprOpt, fieldType, fieldName, valueExpr) -> visitObjArg memberCallHandler objExprOpt
| IfThenElse(guardExpr, thenExpr, elseExpr) ->
visitExpr memberCallHandler guardExpr
visitExpr memberCallHandler thenExpr
visitExpr memberCallHandler elseExpr
| Lambda(lambdaVar, bodyExpr) -> visitExpr memberCallHandler bodyExpr
| Let((bindingVar, bindingExpr, debugPointAtBinding), bodyExpr) ->
visitExpr memberCallHandler bindingExpr
visitExpr memberCallHandler bodyExpr
| LetRec(recursiveBindings, bodyExpr) ->
let recursiveBindings' =
recursiveBindings |> List.map (fun (mfv, expr, dp) -> (mfv, expr))

List.iter (snd >> visitExpr memberCallHandler) recursiveBindings'
visitExpr memberCallHandler bodyExpr
| NewArray(arrayType, argExprs) -> visitExprs memberCallHandler argExprs
| NewDelegate(delegateType, delegateBodyExpr) -> visitExpr memberCallHandler delegateBodyExpr
| NewObject(objType, typeArgs, argExprs) -> visitExprs memberCallHandler argExprs
| NewRecord(recordType, argExprs) -> visitExprs memberCallHandler argExprs
| NewTuple(tupleType, argExprs) -> visitExprs memberCallHandler argExprs
| NewUnionCase(unionType, unionCase, argExprs) -> visitExprs memberCallHandler argExprs
| Quote(quotedExpr) -> visitExpr memberCallHandler quotedExpr
| FSharpFieldGet(objExprOpt, recordOrClassType, fieldInfo) -> visitObjArg memberCallHandler objExprOpt
| FSharpFieldSet(objExprOpt, recordOrClassType, fieldInfo, argExpr) ->
visitObjArg memberCallHandler objExprOpt
visitExpr memberCallHandler argExpr
| Sequential(firstExpr, secondExpr) ->
visitExpr memberCallHandler firstExpr
visitExpr memberCallHandler secondExpr
| TryFinally(bodyExpr, finalizeExpr, debugPointAtTry, debugPointAtFinally) ->
visitExpr memberCallHandler bodyExpr
visitExpr memberCallHandler finalizeExpr
| TryWith(bodyExpr, _, _, catchVar, catchExpr, debugPointAtTry, debugPointAtWith) ->
visitExpr memberCallHandler bodyExpr
visitExpr memberCallHandler catchExpr
| TupleGet(tupleType, tupleElemIndex, tupleExpr) -> visitExpr memberCallHandler tupleExpr
| DecisionTree(decisionExpr, decisionTargets) ->
visitExpr memberCallHandler decisionExpr
List.iter (snd >> visitExpr memberCallHandler) decisionTargets
| DecisionTreeSuccess(decisionTargetIdx, decisionTargetExprs) -> visitExprs memberCallHandler decisionTargetExprs
| TypeLambda(genericParam, bodyExpr) -> visitExpr memberCallHandler bodyExpr
| TypeTest(ty, inpExpr) -> visitExpr memberCallHandler inpExpr
| UnionCaseSet(unionExpr, unionType, unionCase, unionCaseField, valueExpr) ->
visitExpr memberCallHandler unionExpr
visitExpr memberCallHandler valueExpr
| UnionCaseGet(unionExpr, unionType, unionCase, unionCaseField) -> visitExpr memberCallHandler unionExpr
| UnionCaseTest(unionExpr, unionType, unionCase) -> visitExpr memberCallHandler unionExpr
| UnionCaseTag(unionExpr, unionType) -> visitExpr memberCallHandler unionExpr
| ObjectExpr(objType, baseCallExpr, overrides, interfaceImplementations) ->
visitExpr memberCallHandler baseCallExpr
List.iter (visitObjMember memberCallHandler) overrides
List.iter (snd >> List.iter (visitObjMember memberCallHandler)) interfaceImplementations
| TraitCall(sourceTypes, traitName, typeArgs, typeInstantiation, argTypes, argExprs) ->
visitExprs memberCallHandler argExprs
| ValueSet(valToSet, valueExpr) -> visitExpr memberCallHandler valueExpr
| WhileLoop(guardExpr, bodyExpr, debugPointAtWhile) ->
visitExpr memberCallHandler guardExpr
visitExpr memberCallHandler bodyExpr
| BaseValue baseType -> ()
| DefaultValue defaultType -> ()
| ThisValue thisType -> ()
| Const(constValueObj, constType) -> ()
| Value(valueToGet) -> ()
| _ -> ()

and visitExprs f exprs = List.iter (visitExpr f) exprs

and visitObjArg f objOpt = Option.iter (visitExpr f) objOpt

and visitObjMember f memb = visitExpr f memb.Body

let rec visitDeclaration f d =
match d with
| FSharpImplementationFileDeclaration.Entity(e, subDecls) ->
for subDecl in subDecls do
visitDeclaration f subDecl
| FSharpImplementationFileDeclaration.MemberOrFunctionOrValue(v, vs, e) -> visitExpr f e
| FSharpImplementationFileDeclaration.InitAction(e) -> visitExpr f e

let notUsed () =
let option: Option<int> = None
option.Value
Expand All @@ -115,15 +15,19 @@ let optionValueAnalyzer: Analyzer<CliContext> =
async {
let state = ResizeArray<range>()

let handler (range: range) (m: FSharpMemberOrFunctionOrValue) =
let name = String.Join(".", m.DeclaringEntity.Value.FullName, m.DisplayName)
let walker =
{ new TypedTreeCollectorBase() with
override _.WalkCall range m _ =
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
}

match ctx.TypedTree with
| None -> ()
| Some typedTree -> typedTree.Declarations |> List.iter (visitDeclaration handler)
| Some typedTree -> typedTree.Declarations |> List.iter (walkTast walker)

return
state
Expand Down
Loading

0 comments on commit 6a1815a

Please sign in to comment.