Skip to content

Commit

Permalink
Update StringAnalyzer to latest changes.
Browse files Browse the repository at this point in the history
  • Loading branch information
nojaf committed Oct 4, 2023
1 parent 8e60815 commit 1087405
Show file tree
Hide file tree
Showing 9 changed files with 276 additions and 254 deletions.
5 changes: 3 additions & 2 deletions src/FSharp.Analyzers/FSharp.Analyzers.fsproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
Expand All @@ -11,9 +11,10 @@
<ItemGroup>
<Compile Include="ASTCollecting.fs"/>
<Compile Include="TASTCollecting.fs" />
<Compile Include="StringAnalyzers.fs"/>
<Compile Include="JsonSerializerOptionsAnalyzer.fs" />
<Compile Include="UnionCaseAnalyzer.fs" />
<Compile Include="StringAnalyzer.fsi" />
<Compile Include="StringAnalyzer.fs" />
</ItemGroup>

<ItemGroup>
Expand Down
192 changes: 192 additions & 0 deletions src/FSharp.Analyzers/StringAnalyzer.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
module GR.FSharp.Analyzers.StringAnalyzer

open FSharp.Analyzers.SDK
open FSharp.Compiler.CodeAnalysis
open FSharp.Compiler.Symbols
open FSharp.Compiler.Syntax
open FSharp.Compiler.Text

[<Literal>]
let StringEndsWithCode = "GRA-STRING-001"

[<Literal>]
let StringStartsWithCode = "GRA-STRING-002"

[<Literal>]
let StringIndexOfCode = "GRA-STRING-003"

let (|SingleNameInSynLongIdent|_|) name (lid : SynLongIdent) =
match lid with
| SynLongIdent (id = [ ident ]) when ident.idText = name -> Some ident.idRange
| _ -> None

let (|SynLongIdentEndsWith|_|) name (lid : SynLongIdent) =
List.tryLast lid.LongIdent
|> Option.bind (fun ident -> if ident.idText = name then Some ident.idRange else None)

let rec (|SingleStringArgumentExpr|_|) =
function
// Strip parentheses
| SynExpr.Paren (expr = SingleStringArgumentExpr) -> Some ()
// Only allow ""
| SynExpr.Const (constant = constant) ->
match constant with
| SynConst.String _ -> Some ()
| _ -> None
// Don't allow tuples and any other obvious non value expression
| SynExpr.Tuple _
| SynExpr.Lambda _
| SynExpr.MatchLambda _
| SynExpr.MatchBang _
| SynExpr.LetOrUseBang _
| SynExpr.AnonRecd _
| SynExpr.ArrayOrList _
| SynExpr.ArrayOrListComputed _
| SynExpr.Assert _
| SynExpr.DoBang _
| SynExpr.DotSet _
| SynExpr.For _
| SynExpr.ForEach _
| SynExpr.Lazy _
| SynExpr.Record _
| SynExpr.Set _
| SynExpr.While _
| SynExpr.YieldOrReturn _
| SynExpr.YieldOrReturnFrom _ -> None
// Allow pretty much any expression
| _ -> Some ()

let findAllInvocations (parameterPredicate : SynExpr -> bool) (functionName : string) (ast : ParsedInput) : range list =
let collector = ResizeArray<range> ()

let walker =
{ new SyntaxCollectorBase() with
override _.WalkExpr (expr : SynExpr) =
match expr with
// "".FunctionName arg
| SynExpr.App (
isInfix = false
funcExpr = SynExpr.DotGet (longDotId = SingleNameInSynLongIdent functionName mFunctionName)
argExpr = argExpr)

// w.FunctionName arg
| SynExpr.App (
funcExpr = SynExpr.LongIdent (longDotId = SynLongIdentEndsWith functionName mFunctionName)
argExpr = argExpr) when parameterPredicate argExpr -> collector.Add mFunctionName

| _ -> ()
}

walkAst walker ast

collector |> Seq.toList

let invalidStringFunctionUseAnalyzer
functionName
code
message
severity
(sourceText : ISourceText)
(untypedTree : ParsedInput)
(checkFileResults : FSharpCheckFileResults)
(unTypedArgumentPredicate : SynExpr -> bool)
(typedArgumentPredicate : FSharpMemberOrFunctionOrValue -> bool)
=
async {
let invocations =
findAllInvocations unTypedArgumentPredicate functionName untypedTree

return
invocations
|> List.choose (fun mEndsWith ->
let lineText = sourceText.GetLineString (mEndsWith.EndLine - 1)

let symbolUseOpt =
checkFileResults.GetSymbolUseAtLocation (
mEndsWith.EndLine,
mEndsWith.EndColumn,
lineText,
[ "EndsWith" ]
)

symbolUseOpt
|> Option.bind (fun symbolUse ->
match symbolUse.Symbol with
| :? FSharpMemberOrFunctionOrValue as mfv ->
if mfv.Assembly.SimpleName <> "netstandard" then
None
elif not (mfv.FullName = $"System.String.%s{functionName}") then
None
elif not (typedArgumentPredicate mfv) then
None
else
Some mEndsWith
| _ -> None
)
)
|> List.map (fun mFunctionName ->
{
Type = $"String.{functionName} analyzer"
Message = message
Code = code
Severity = severity
Range = mFunctionName
Fixes = []
}
)
}

let hasMatchingSignature (expectedSignature : string) (mfv : FSharpMemberOrFunctionOrValue) : bool =
let rec visit (fsharpType : FSharpType) =
if fsharpType.GenericArguments.Count = 0 then
fsharpType.ErasedType.BasicQualifiedName
else
fsharpType.GenericArguments |> Seq.map visit |> String.concat " -> "

let actualSignature = visit mfv.FullType
actualSignature = expectedSignature

[<CliAnalyzer "String.EndsWith Analyzer">]
let endsWithAnalyzer (ctx : CliContext) : Async<Message list> =
invalidStringFunctionUseAnalyzer
"EndsWith"
StringEndsWithCode
"The usage of String.EndsWith with a single string argument is discouraged. Signal your intention explicitly by calling an overload."
Warning
ctx.SourceText
ctx.ParseFileResults.ParseTree
ctx.CheckFileResults
(function
| SingleStringArgumentExpr _ -> true
| _ -> false)
(hasMatchingSignature "System.String -> System.Boolean")

[<CliAnalyzer "String.StartsWith Analyzer">]
let startsWithAnalyzer (ctx : CliContext) : Async<Message list> =
invalidStringFunctionUseAnalyzer
"StartsWith"
StringStartsWithCode
"The usage of String.StartsWith with a single string argument is discouraged. Signal your intention explicitly by calling an overload."
Warning
ctx.SourceText
ctx.ParseFileResults.ParseTree
ctx.CheckFileResults
(function
| SingleStringArgumentExpr _ -> true
| _ -> false)
(hasMatchingSignature "System.String -> System.Boolean")

[<CliAnalyzer "String.IndexOf Analyzer">]
let indexOfAnalyzer (ctx : CliContext) : Async<Message list> =
invalidStringFunctionUseAnalyzer
"IndexOf"
StringIndexOfCode
"The usage of String.IndexOf with a single string argument is discouraged. Signal your intention explicitly by calling an overload."
Warning
ctx.SourceText
ctx.ParseFileResults.ParseTree
ctx.CheckFileResults
(function
| SingleStringArgumentExpr _ -> true
| _ -> false)
(hasMatchingSignature "System.String -> System.Int32")
21 changes: 21 additions & 0 deletions src/FSharp.Analyzers/StringAnalyzer.fsi
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
module GR.FSharp.Analyzers.StringAnalyzer

open FSharp.Analyzers.SDK

[<Literal>]
val StringEndsWithCode : string = "GRA-STRING-001"

[<Literal>]
val StringStartsWithCode : string = "GRA-STRING-002"

[<Literal>]
val StringIndexOfCode : string = "GRA-STRING-003"

[<CliAnalyzer "String.EndsWith Analyzer">]
val endsWithAnalyzer : ctx : CliContext -> Async<Message list>

[<CliAnalyzer "String.StartsWith Analyzer">]
val startsWithAnalyzer : ctx : CliContext -> Async<Message list>

[<CliAnalyzer "String.IndexOf Analyzer">]
val indexOfAnalyzer : ctx : CliContext -> Async<Message list>
Loading

0 comments on commit 1087405

Please sign in to comment.