-
-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #20 from dawedawe/add_emptystringanalyzer
Add EmptyStringAnalyzer
- Loading branch information
Showing
6 changed files
with
236 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
--- | ||
title: EmptyString | ||
category: suggestion | ||
categoryindex: 2 | ||
index: 4 | ||
--- | ||
# EmptyStringAnalyzer | ||
|
||
## Problem | ||
|
||
Testing symbolically for an empty string is not the most efficient way. | ||
Using the `String.Length` property or the `String.IsNullOrEmpty` method is the preferred way. | ||
This is a port of the Roslyn analyzer [ca1820](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1820) | ||
|
||
```fsharp | ||
let s = "foo" | ||
let b = s = "" // Triggers analyzer | ||
``` | ||
|
||
## Fix | ||
|
||
Use the `Length` property if you know the reference is not null or the `String.IsNullOrEmpty` method otherwise. | ||
|
||
```fsharp | ||
let s = "foo" | ||
let b = s.Length = 0 | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
module Ionide.Analyzers.Suggestion.EmptyStringAnalyzer | ||
|
||
open FSharp.Analyzers.SDK | ||
open FSharp.Analyzers.SDK.TASTCollecting | ||
open FSharp.Compiler.Symbols | ||
open FSharp.Compiler.Text | ||
|
||
let (|EmptyStringConst|_|) (e: FSharpExpr) = | ||
if e.Type.ErasedType.BasicQualifiedName = "System.String" then | ||
match e with | ||
| FSharpExprPatterns.Const(o, _type) when not (isNull o) && (string o).Length = 0 -> Some() | ||
| _ -> None | ||
else | ||
None | ||
|
||
let invalidStringFunctionUseAnalyzer (typedTree: FSharpImplementationFileContents) = | ||
let ranges = ResizeArray<range>() | ||
|
||
let walker = | ||
{ new TypedTreeCollectorBase() with | ||
override _.WalkCall _ (mfv: FSharpMemberOrFunctionOrValue) _ _ (args: FSharpExpr list) (m: range) = | ||
match (mfv.Assembly.SimpleName, mfv.FullName, args) with | ||
| "FSharp.Core", "Microsoft.FSharp.Core.Operators.(=)", [ _; EmptyStringConst ] | ||
| "FSharp.Core", "Microsoft.FSharp.Core.Operators.(=)", [ EmptyStringConst; _ ] -> ranges.Add m | ||
| _ -> () | ||
|
||
} | ||
|
||
walkTast walker typedTree | ||
|
||
ranges | ||
|> Seq.map (fun r -> | ||
{ | ||
Type = "EmptyString analyzer" | ||
Message = | ||
"Test for empty strings should use the String.Length property or the String.IsNullOrEmpty method." | ||
Code = "IONIDE-005" | ||
Severity = Warning | ||
Range = r | ||
Fixes = [] | ||
} | ||
) | ||
|> Seq.toList | ||
|
||
[<EditorAnalyzer("EmptyStringAnalyzer", | ||
"Verifies testing for an empty string is done efficiently.", | ||
"https://ionide.io/ionide-analyzers/suggestion/005.html")>] | ||
let emptyStringEditorAnalyzer (ctx: EditorContext) = | ||
async { | ||
return | ||
ctx.TypedTree | ||
|> Option.map invalidStringFunctionUseAnalyzer | ||
|> Option.defaultValue [] | ||
} | ||
|
||
[<CliAnalyzer("EmptyStringAnalyzer", | ||
"Verifies testing for an empty string is done efficiently.", | ||
"https://ionide.io/ionide-analyzers/suggestion/005.html")>] | ||
let emptyStringCliAnalyzer (ctx: CliContext) = | ||
async { | ||
return | ||
ctx.TypedTree | ||
|> Option.map invalidStringFunctionUseAnalyzer | ||
|> Option.defaultValue [] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
140 changes: 140 additions & 0 deletions
140
tests/Ionide.Analyzers.Tests/Suggestion/EmptyStringAnalyzerTests.fs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
module Ionide.Analyzers.Tests.Suggestion.EmptyStringAnalyzerTests | ||
|
||
open NUnit.Framework | ||
open FSharp.Compiler.CodeAnalysis | ||
open FSharp.Analyzers.SDK.Testing | ||
open Ionide.Analyzers.Suggestion.EmptyStringAnalyzer | ||
|
||
let mutable projectOptions: FSharpProjectOptions = FSharpProjectOptions.zero | ||
|
||
[<SetUp>] | ||
let Setup () = | ||
task { | ||
let! opts = mkOptionsFromProject "net7.0" [] | ||
|
||
projectOptions <- opts | ||
} | ||
|
||
[<Test>] | ||
let ``Operator based test for zero-length`` () = | ||
async { | ||
let source = | ||
""" | ||
module M | ||
let s = "foo" | ||
let x = s = "" | ||
""" | ||
|
||
let ctx = getContext projectOptions source | ||
let! msgs = emptyStringCliAnalyzer ctx | ||
Assert.IsNotEmpty msgs | ||
|
||
Assert.IsTrue( | ||
Assert.messageContains | ||
"Test for empty strings should use the String.Length property or the String.IsNullOrEmpty method." | ||
msgs[0] | ||
) | ||
} | ||
|
||
[<Test>] | ||
let ``Operator based test for zero-length reversed`` () = | ||
async { | ||
let source = | ||
""" | ||
module M | ||
let s = "foo" | ||
let x = "" = s | ||
""" | ||
|
||
let ctx = getContext projectOptions source | ||
let! msgs = emptyStringCliAnalyzer ctx | ||
Assert.IsNotEmpty msgs | ||
|
||
Assert.IsTrue( | ||
Assert.messageContains | ||
"Test for empty strings should use the String.Length property or the String.IsNullOrEmpty method." | ||
msgs[0] | ||
) | ||
} | ||
|
||
[<Test>] | ||
let ``Operator based equality test`` () = | ||
async { | ||
let source = | ||
""" | ||
module M | ||
let s = "foo" | ||
let x = s = "bar" | ||
""" | ||
|
||
let ctx = getContext projectOptions source | ||
let! msgs = emptyStringCliAnalyzer ctx | ||
Assert.IsEmpty msgs | ||
} | ||
|
||
[<Test>] | ||
let ``Operator based equality test reversed`` () = | ||
async { | ||
let source = | ||
""" | ||
module M | ||
let s = "foo" | ||
let x = "bar" = s | ||
""" | ||
|
||
let ctx = getContext projectOptions source | ||
let! msgs = emptyStringCliAnalyzer ctx | ||
Assert.IsEmpty msgs | ||
} | ||
|
||
[<Test>] | ||
let ``Operator based null test`` () = | ||
async { | ||
let source = | ||
""" | ||
module M | ||
let s = "foo" | ||
let x = s = null | ||
""" | ||
|
||
let ctx = getContext projectOptions source | ||
let! msgs = emptyStringCliAnalyzer ctx | ||
Assert.IsEmpty msgs | ||
} | ||
|
||
[<Test>] | ||
let ``Operator based null test reversed`` () = | ||
async { | ||
let source = | ||
""" | ||
module M | ||
let s = "foo" | ||
let x = null = s | ||
""" | ||
|
||
let ctx = getContext projectOptions source | ||
let! msgs = emptyStringCliAnalyzer ctx | ||
Assert.IsEmpty msgs | ||
} | ||
|
||
[<Test>] | ||
let ``Property based length test`` () = | ||
async { | ||
let source = | ||
""" | ||
module M | ||
let s = "foo" | ||
let x = s.Length = 0 | ||
""" | ||
|
||
let ctx = getContext projectOptions source | ||
let! msgs = emptyStringCliAnalyzer ctx | ||
Assert.IsEmpty msgs | ||
} |