Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

String analyzers #3

Merged
merged 13 commits into from
Oct 6, 2023
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