Skip to content

Commit

Permalink
String analyzers (#3)
Browse files Browse the repository at this point in the history
* Extract EndsWith functionality.

* Add String.StartsWith analyzer

* Use FSharp.Analyzers.SDK.Testing

* Restore project reference

* Assert function signature.

* Add positive test for String.IndexOf.

* Update StringAnalyzer to latest changes.

* Correctly run other string tests.

* Add negative tests for String.IndexOf

* Rename Testing to Common.

* Remove old files

* Update string analyzers to use the typed tree.

* Add unit tests for overloads.
  • Loading branch information
nojaf authored Oct 6, 2023
1 parent d8cca37 commit a0fa92e
Show file tree
Hide file tree
Showing 42 changed files with 279 additions and 161 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
127 changes: 127 additions & 0 deletions src/FSharp.Analyzers/StringAnalyzer.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
module GR.FSharp.Analyzers.StringAnalyzer

open FSharp.Analyzers.SDK
open FSharp.Compiler.Symbols
open FSharp.Compiler.Text
open GR.FSharp.Analyzers.TASTCollecting

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

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

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

let (|StringExpr|_|) (e : FSharpExpr) =
if e.Type.ErasedType.BasicQualifiedName = "System.String" then
Some ()
else
None

let (|IntExpr|_|) (e : FSharpExpr) =
if e.Type.ErasedType.BasicQualifiedName = "System.Int32" then
Some ()
else
None

let invalidStringFunctionUseAnalyzer
functionName
code
message
severity
(typedTree : FSharpImplementationFileContents)
(typedArgumentPredicate : FSharpExpr list -> bool)
=
let invocations = ResizeArray<range> ()

let handler : Handler =
Handler.CallHandler (fun (m : range) (mfv : FSharpMemberOrFunctionOrValue) (args : FSharpExpr list) ->
if
mfv.Assembly.SimpleName = "System.Runtime"
&& mfv.FullName = $"System.String.{functionName}"
&& typedArgumentPredicate args
then
invocations.Add m
)

for decl in typedTree.Declarations do
visitDeclaration handler decl

invocations
|> Seq.map (fun mFunctionName ->
{
Type = $"String.{functionName} analyzer"
Message = message
Code = code
Severity = severity
Range = mFunctionName
Fixes = []
}
)
|> Seq.toList

[<CliAnalyzer "String.EndsWith Analyzer">]
let endsWithAnalyzer (ctx : CliContext) : Async<Message list> =
async {
match ctx.TypedTree with
| None -> return List.empty
| Some typedTree ->

return
invalidStringFunctionUseAnalyzer
"EndsWith"
StringEndsWithCode
"The usage of String.EndsWith with a single string argument is discouraged. Signal your intention explicitly by calling an overload."
Warning
typedTree
(fun (args : FSharpExpr list) ->
match args with
| [ StringExpr ] -> true
| _ -> false
)
}

[<CliAnalyzer "String.StartsWith Analyzer">]
let startsWithAnalyzer (ctx : CliContext) : Async<Message list> =
async {
match ctx.TypedTree with
| None -> return List.empty
| Some typedTree ->
return
invalidStringFunctionUseAnalyzer
"StartsWith"
StringStartsWithCode
"The usage of String.StartsWith with a single string argument is discouraged. Signal your intention explicitly by calling an overload."
Warning
typedTree
(fun (args : FSharpExpr list) ->
match args with
| [ StringExpr ] -> true
| _ -> false
)
}

[<CliAnalyzer "String.IndexOf Analyzer">]
let indexOfAnalyzer (ctx : CliContext) : Async<Message list> =
async {
match ctx.TypedTree with
| None -> return List.empty
| Some typedTree ->

return
invalidStringFunctionUseAnalyzer
"IndexOf"
StringIndexOfCode
"The usage of String.IndexOf with a single string argument is discouraged. Signal your intention explicitly by calling an overload."
Warning
typedTree
(fun args ->
match args with
| [ StringExpr ]
| [ StringExpr ; IntExpr ]
| [ StringExpr ; IntExpr ; IntExpr ] -> true
| _ -> false
)
}
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>
106 changes: 0 additions & 106 deletions src/FSharp.Analyzers/StringAnalyzers.fs

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

open System
open System.IO
open System.Collections
open System.Threading.Tasks
open NUnit.Framework
open FSharp.Analyzers.SDK
Expand Down Expand Up @@ -46,12 +47,16 @@ let assertExpected sourceFile messages =

let dataFolder = Path.Combine (__SOURCE_DIRECTORY__, "..", "data")

let constructTestCaseEnumerator (subDataPath : string array) =
let testsDirectory = Path.Combine (dataFolder, Path.Combine subDataPath)

Directory.EnumerateFiles (testsDirectory, "*.fs")
let constructTestCaseEnumeratorAux (files : string seq) : IEnumerator =
files
|> Seq.map (fun f ->
let fileName = Path.GetRelativePath (dataFolder, f)
[| fileName :> obj |]
)
|> fun s -> s.GetEnumerator ()

let constructTestCaseEnumerator (subDataPath : string array) =
let testsDirectory = Path.Combine (dataFolder, Path.Combine subDataPath)

Directory.EnumerateFiles (testsDirectory, "*.fs")
|> constructTestCaseEnumeratorAux
4 changes: 2 additions & 2 deletions tests/FSharp.Analyzers.Tests/FSharp.Analyzers.Tests.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@
</PropertyGroup>

<ItemGroup>
<Compile Include="Testing.fs" />
<Compile Include="StringAnalyzerTests.fs" />
<Compile Include="Common.fs" />
<Compile Include="JsonSerializerOptionsAnalyzerTests.fs" />
<Compile Include="UnionCaseAnalyzerTests.fs" />
<Compile Include="StringAnalyzerTests.fs" />
</ItemGroup>

<ItemGroup>
Expand Down
Loading

0 comments on commit a0fa92e

Please sign in to comment.