Skip to content

Commit 960d218

Browse files
authored
Merge pull request #64 from dawedawe/add_editor_support
add editor support to all analyzers
2 parents b7bf8f5 + e3aca86 commit 960d218

10 files changed

+243
-198
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Changelog
22

3+
## 0.6.1 - 2024-01-01
4+
5+
### Added
6+
* Add editor support to all analyzers. [#64](https://github.com/ionide/ionide-analyzers/pull/64)
7+
38
## 0.6.0 - 2023-12-20
49

510
### Changed

src/Ionide.Analyzers/Suggestion/CopyAndUpdateRecordChangesAllFieldsAnalyzer.fs

Lines changed: 65 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -9,57 +9,69 @@ open FSharp.Compiler.Syntax
99

1010
type UpdateRecord = SynExprRecordField list * range
1111

12-
[<CliAnalyzer("CopyAndUpdateRecordChangesAllFieldsAnalyzer",
13-
"Detect if all fields in a record update expression are updated.",
14-
"https://ionide.io/ionide-analyzers/suggestion/001.html")>]
15-
let copyAndUpdateRecordChangesAllFieldsAnalyzer: Analyzer<CliContext> =
16-
fun (context: CliContext) ->
17-
async {
18-
let untypedRecordUpdates =
19-
let xs = ResizeArray<UpdateRecord>()
20-
21-
let collector =
22-
{ new SyntaxCollectorBase() with
23-
override x.WalkExpr(_, e: SynExpr) =
24-
match e with
25-
| SynExpr.Record(copyInfo = Some _; recordFields = fields) -> xs.Add(fields, e.Range)
26-
| _ -> ()
27-
}
28-
29-
walkAst collector context.ParseFileResults.ParseTree
30-
Seq.toList xs
31-
32-
let messages = ResizeArray<Message> untypedRecordUpdates.Length
33-
34-
let tastCollector =
35-
{ new TypedTreeCollectorBase() with
36-
override x.WalkNewRecord (recordType: FSharpType) _ (mRecord: range) =
37-
let matchingUnTypedNode =
38-
untypedRecordUpdates
39-
|> List.tryFind (fun (_, mExpr) -> Range.equals mExpr mRecord)
40-
41-
match matchingUnTypedNode with
42-
| None -> ()
43-
| Some(fields, mExpr) ->
44-
45-
if not recordType.TypeDefinition.IsFSharpRecord then
46-
()
47-
else if recordType.TypeDefinition.FSharpFields.Count = fields.Length then
48-
messages.Add
49-
{
50-
Type = "CopyAndUpdateRecordChangesAllFieldsAnalyzer analyzer"
51-
Message =
52-
"All record fields of record are being updated. Consider creating a new instance instead."
53-
Code = "IONIDE-001"
54-
Severity = Severity.Hint
55-
Range = mExpr
56-
Fixes = []
57-
}
58-
}
59-
60-
match context.TypedTree with
61-
| None -> ()
62-
| Some typedTree -> walkTast tastCollector typedTree
63-
64-
return Seq.toList messages
12+
let analyze parseTree (typedTree: FSharpImplementationFileContents option) =
13+
let untypedRecordUpdates =
14+
let xs = ResizeArray<UpdateRecord>()
15+
16+
let collector =
17+
{ new SyntaxCollectorBase() with
18+
override x.WalkExpr(_, e: SynExpr) =
19+
match e with
20+
| SynExpr.Record(copyInfo = Some _; recordFields = fields) -> xs.Add(fields, e.Range)
21+
| _ -> ()
22+
}
23+
24+
walkAst collector parseTree
25+
Seq.toList xs
26+
27+
let messages = ResizeArray<Message> untypedRecordUpdates.Length
28+
29+
let tastCollector =
30+
{ new TypedTreeCollectorBase() with
31+
override x.WalkNewRecord (recordType: FSharpType) _ (mRecord: range) =
32+
let matchingUnTypedNode =
33+
untypedRecordUpdates
34+
|> List.tryFind (fun (_, mExpr) -> Range.equals mExpr mRecord)
35+
36+
match matchingUnTypedNode with
37+
| None -> ()
38+
| Some(fields, mExpr) ->
39+
40+
if not recordType.TypeDefinition.IsFSharpRecord then
41+
()
42+
else if recordType.TypeDefinition.FSharpFields.Count = fields.Length then
43+
messages.Add
44+
{
45+
Type = "CopyAndUpdateRecordChangesAllFieldsAnalyzer analyzer"
46+
Message =
47+
"All record fields of record are being updated. Consider creating a new instance instead."
48+
Code = "IONIDE-001"
49+
Severity = Severity.Hint
50+
Range = mExpr
51+
Fixes = []
52+
}
6553
}
54+
55+
match typedTree with
56+
| None -> ()
57+
| Some typedTree -> walkTast tastCollector typedTree
58+
59+
Seq.toList messages
60+
61+
[<Literal>]
62+
let name = "CopyAndUpdateRecordChangesAllFieldsAnalyzer"
63+
64+
[<Literal>]
65+
let shortDescription =
66+
"Detect if all fields in a record update expression are updated."
67+
68+
[<Literal>]
69+
let helpUri = "https://ionide.io/ionide-analyzers/suggestion/001.html"
70+
71+
[<CliAnalyzer(name, shortDescription, helpUri)>]
72+
let copyAndUpdateRecordChangesAllFieldsCliAnalyzer: Analyzer<CliContext> =
73+
fun (context: CliContext) -> async { return analyze context.ParseFileResults.ParseTree context.TypedTree }
74+
75+
[<EditorAnalyzer(name, shortDescription, helpUri)>]
76+
let copyAndUpdateRecordChangesAllFieldsEditorAnalyzer: Analyzer<EditorContext> =
77+
fun (context: EditorContext) -> async { return analyze context.ParseFileResults.ParseTree context.TypedTree }

src/Ionide.Analyzers/Suggestion/EmptyStringAnalyzer.fs

Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ let (|EmptyStringConst|_|) (e: FSharpExpr) =
1212
| Some("System.String"), FSharpExprPatterns.Const(o, _type) when not (isNull o) && (string o).Length = 0 -> Some()
1313
| _ -> None
1414

15-
let invalidStringFunctionUseAnalyzer (typedTree: FSharpImplementationFileContents) =
15+
let analyze (typedTree: FSharpImplementationFileContents) =
1616
let ranges = ResizeArray<range>()
1717

1818
let walker =
@@ -41,24 +41,19 @@ let invalidStringFunctionUseAnalyzer (typedTree: FSharpImplementationFileContent
4141
)
4242
|> Seq.toList
4343

44-
[<EditorAnalyzer("EmptyStringAnalyzer",
45-
"Verifies testing for an empty string is done efficiently.",
46-
"https://ionide.io/ionide-analyzers/suggestion/005.html")>]
44+
[<Literal>]
45+
let name = "EmptyStringAnalyzer"
46+
47+
[<Literal>]
48+
let shortDescription = "Verifies testing for an empty string is done efficiently."
49+
50+
[<Literal>]
51+
let helpUri = "https://ionide.io/ionide-analyzers/suggestion/005.html"
52+
53+
[<EditorAnalyzer(name, shortDescription, helpUri)>]
4754
let emptyStringEditorAnalyzer (ctx: EditorContext) =
48-
async {
49-
return
50-
ctx.TypedTree
51-
|> Option.map invalidStringFunctionUseAnalyzer
52-
|> Option.defaultValue []
53-
}
55+
async { return ctx.TypedTree |> Option.map analyze |> Option.defaultValue [] }
5456

55-
[<CliAnalyzer("EmptyStringAnalyzer",
56-
"Verifies testing for an empty string is done efficiently.",
57-
"https://ionide.io/ionide-analyzers/suggestion/005.html")>]
57+
[<CliAnalyzer(name, shortDescription, helpUri)>]
5858
let emptyStringCliAnalyzer (ctx: CliContext) =
59-
async {
60-
return
61-
ctx.TypedTree
62-
|> Option.map invalidStringFunctionUseAnalyzer
63-
|> Option.defaultValue []
64-
}
59+
async { return ctx.TypedTree |> Option.map analyze |> Option.defaultValue [] }

src/Ionide.Analyzers/Suggestion/HandleOptionGracefullyAnalyzer.fs

Lines changed: 54 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -6,46 +6,57 @@ open FSharp.Analyzers.SDK.TASTCollecting
66
open FSharp.Compiler.Symbols
77
open FSharp.Compiler.Text
88

9-
[<CliAnalyzer("HandleOptionGracefullyAnalyzer",
10-
"Replace unsafe option unwrapping with graceful handling of each case.",
11-
"https://ionide.io/ionide-analyzers/suggestion/006.html")>]
12-
let optionGetAnalyzer (ctx: CliContext) =
13-
async {
14-
let messages = ResizeArray<Message>()
15-
16-
let walker =
17-
{ new TypedTreeCollectorBase() with
18-
override x.WalkCall _ (mfv: FSharpMemberOrFunctionOrValue) _ _ (args: FSharpExpr list) (m: range) =
19-
let fullyQualifiedCall =
20-
let fullName =
21-
mfv.DeclaringEntity
22-
|> Option.map (fun e -> e.TryGetFullName())
23-
|> Option.flatten
24-
|> Option.defaultValue ""
25-
26-
String.Join(".", fullName, mfv.DisplayName)
27-
28-
if
29-
(mfv.FullName = "Microsoft.FSharp.Core.Option.get"
30-
|| mfv.FullName = "Microsoft.FSharp.Core.ValueOption.get"
31-
|| fullyQualifiedCall = "Microsoft.FSharp.Core.FSharpOption`1.Value"
32-
|| fullyQualifiedCall = "Microsoft.FSharp.Core.FSharpValueOption`1.Value")
33-
&& args.Length = 1
34-
then
35-
messages.Add
36-
{
37-
Type = "HandleOptionGracefully"
38-
Message = "Replace unsafe option unwrapping with graceful handling of each case."
39-
Code = "IONIDE-006"
40-
Severity = Severity.Warning
41-
Range = m
42-
Fixes = []
43-
}
44-
}
45-
46-
match ctx.TypedTree with
47-
| None -> return []
48-
| Some typedTree ->
49-
walkTast walker typedTree
50-
return Seq.toList messages
51-
}
9+
let analyze (typedTree: FSharpImplementationFileContents option) =
10+
let messages = ResizeArray<Message>()
11+
12+
let walker =
13+
{ new TypedTreeCollectorBase() with
14+
override x.WalkCall _ (mfv: FSharpMemberOrFunctionOrValue) _ _ (args: FSharpExpr list) (m: range) =
15+
let fullyQualifiedCall =
16+
let fullName =
17+
mfv.DeclaringEntity
18+
|> Option.map (fun e -> e.TryGetFullName())
19+
|> Option.flatten
20+
|> Option.defaultValue ""
21+
22+
String.Join(".", fullName, mfv.DisplayName)
23+
24+
if
25+
(mfv.FullName = "Microsoft.FSharp.Core.Option.get"
26+
|| mfv.FullName = "Microsoft.FSharp.Core.ValueOption.get"
27+
|| fullyQualifiedCall = "Microsoft.FSharp.Core.FSharpOption`1.Value"
28+
|| fullyQualifiedCall = "Microsoft.FSharp.Core.FSharpValueOption`1.Value")
29+
&& args.Length = 1
30+
then
31+
messages.Add
32+
{
33+
Type = "HandleOptionGracefully"
34+
Message = "Replace unsafe option unwrapping with graceful handling of each case."
35+
Code = "IONIDE-006"
36+
Severity = Severity.Warning
37+
Range = m
38+
Fixes = []
39+
}
40+
}
41+
42+
match typedTree with
43+
| None -> []
44+
| Some typedTree ->
45+
walkTast walker typedTree
46+
Seq.toList messages
47+
48+
[<Literal>]
49+
let name = "HandleOptionGracefullyAnalyzer"
50+
51+
[<Literal>]
52+
let shortDescription =
53+
"Replace unsafe option unwrapping with graceful handling of each case."
54+
55+
[<Literal>]
56+
let helpUri = "https://ionide.io/ionide-analyzers/suggestion/006.html"
57+
58+
[<CliAnalyzer(name, shortDescription, helpUri)>]
59+
let optionGetCliAnalyzer (ctx: CliContext) = async { return analyze ctx.TypedTree }
60+
61+
[<EditorAnalyzer(name, shortDescription, helpUri)>]
62+
let optionGetEditorAnalyzer (ctx: EditorContext) = async { return analyze ctx.TypedTree }

src/Ionide.Analyzers/Suggestion/IgnoreFunctionAnalyzer.fs

Lines changed: 42 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -5,35 +5,45 @@ open FSharp.Analyzers.SDK.TASTCollecting
55
open FSharp.Compiler.Symbols
66
open FSharp.Compiler.Text
77

8-
[<CliAnalyzer("IgnoreFunctionAnalyzer",
9-
"A function is being ignored. Did you mean to execute this?",
10-
"https://ionide.io/ionide-analyzers/suggestion/003.html")>]
11-
let ignoreFunctionAnalyzer (ctx: CliContext) =
12-
async {
13-
let messages = ResizeArray<Message>()
14-
15-
let tastCollector =
16-
{ new TypedTreeCollectorBase() with
17-
override x.WalkCall _ (mfv: FSharpMemberOrFunctionOrValue) _ _ (args: FSharpExpr list) (m: range) =
18-
if
19-
mfv.FullName = "Microsoft.FSharp.Core.Operators.ignore"
20-
&& args.Length = 1
21-
&& args.[0].Type.IsFunctionType
22-
then
23-
messages.Add
24-
{
25-
Type = "IgnoreFunctionAnalyzer"
26-
Message = "A function is being ignored. Did you mean to execute this?"
27-
Code = "IONIDE-003"
28-
Severity = Severity.Warning
29-
Range = m
30-
Fixes = []
31-
}
32-
}
33-
34-
match ctx.TypedTree with
35-
| None -> return []
36-
| Some typedTree ->
37-
walkTast tastCollector typedTree
38-
return Seq.toList messages
39-
}
8+
let analyzer (typedTree: FSharpImplementationFileContents option) =
9+
let messages = ResizeArray<Message>()
10+
11+
let tastCollector =
12+
{ new TypedTreeCollectorBase() with
13+
override x.WalkCall _ (mfv: FSharpMemberOrFunctionOrValue) _ _ (args: FSharpExpr list) (m: range) =
14+
if
15+
mfv.FullName = "Microsoft.FSharp.Core.Operators.ignore"
16+
&& args.Length = 1
17+
&& args.[0].Type.IsFunctionType
18+
then
19+
messages.Add
20+
{
21+
Type = "IgnoreFunctionAnalyzer"
22+
Message = "A function is being ignored. Did you mean to execute this?"
23+
Code = "IONIDE-003"
24+
Severity = Severity.Warning
25+
Range = m
26+
Fixes = []
27+
}
28+
}
29+
30+
match typedTree with
31+
| None -> []
32+
| Some typedTree ->
33+
walkTast tastCollector typedTree
34+
Seq.toList messages
35+
36+
[<Literal>]
37+
let name = "IgnoreFunctionAnalyzer"
38+
39+
[<Literal>]
40+
let shortDescription = "A function is being ignored. Did you mean to execute this?"
41+
42+
[<Literal>]
43+
let helpUri = "https://ionide.io/ionide-analyzers/suggestion/003.html"
44+
45+
[<CliAnalyzer(name, shortDescription, helpUri)>]
46+
let ignoreFunctionCliAnalyzer (ctx: CliContext) = async { return analyzer ctx.TypedTree }
47+
48+
[<EditorAnalyzer(name, shortDescription, helpUri)>]
49+
let ignoreFunctionEditorAnalyzer (ctx: EditorContext) = async { return analyzer ctx.TypedTree }

0 commit comments

Comments
 (0)