Skip to content

Commit 5cf1cd9

Browse files
vasily-kirichenkoKevinRansom
authored andcommitted
Unused declarations analyzer (dotnet#2358)
* add UnusedDeclarationsAnalyzer * fix compilation * make UnusedDeclarationsAnalyzer work on private symbols only * fix compilation UnusedDeclarationsAnalyzer analyze semantic, not syntax * finish UnusedDeclarationsAnalyzer * do not mark as unused symbols prefixed with underscore * fix FSharp.Editor.Pervasive.isScript * UnusedDeclarationsAnalyzer consider all declaration in scripts as private to file
1 parent 5b3688a commit 5cf1cd9

File tree

5 files changed

+98
-2
lines changed

5 files changed

+98
-2
lines changed

vsintegration/src/FSharp.Editor/Common/Pervasive.fs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@ open System.Diagnostics
99

1010
/// Checks if the filePath ends with ".fsi"
1111
let isSignatureFile (filePath:string) =
12-
Path.GetExtension filePath = ".fsi"
12+
String.Equals (Path.GetExtension filePath, ".fsi", StringComparison.OrdinalIgnoreCase)
1313

1414
/// Checks if the file paht ends with '.fsx' or '.fsscript'
1515
let isScriptFile (filePath:string) =
1616
let ext = Path.GetExtension filePath
17-
String.Equals (ext,".fsi",StringComparison.OrdinalIgnoreCase) || String.Equals (ext,".fsscript",StringComparison.OrdinalIgnoreCase)
17+
String.Equals (ext, ".fsx", StringComparison.OrdinalIgnoreCase) || String.Equals (ext, ".fsscript", StringComparison.OrdinalIgnoreCase)
1818

1919
/// Path combination operator
2020
let (</>) path1 path2 = Path.Combine (path1, path2)
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
2+
3+
namespace rec Microsoft.VisualStudio.FSharp.Editor
4+
5+
open System
6+
open System.Collections.Immutable
7+
open System.Threading.Tasks
8+
open System.Collections.Generic
9+
10+
open Microsoft.CodeAnalysis
11+
open Microsoft.CodeAnalysis.Diagnostics
12+
open Microsoft.FSharp.Compiler.SourceCodeServices
13+
14+
[<DiagnosticAnalyzer(FSharpConstants.FSharpLanguageName)>]
15+
type internal UnusedDeclarationsAnalyzer() =
16+
inherit DocumentDiagnosticAnalyzer()
17+
18+
let getProjectInfoManager (document: Document) = document.Project.Solution.Workspace.Services.GetService<FSharpCheckerWorkspaceService>().ProjectInfoManager
19+
let getChecker (document: Document) = document.Project.Solution.Workspace.Services.GetService<FSharpCheckerWorkspaceService>().Checker
20+
let [<Literal>] DescriptorId = "FS1182"
21+
22+
let Descriptor =
23+
DiagnosticDescriptor(
24+
id = DescriptorId,
25+
title = SR.TheValueIsUnused.Value,
26+
messageFormat = SR.TheValueIsUnused.Value,
27+
category = DiagnosticCategory.Style,
28+
defaultSeverity = DiagnosticSeverity.Hidden,
29+
isEnabledByDefault = true,
30+
customTags = DiagnosticCustomTags.Unnecessary)
31+
32+
let symbolUseComparer =
33+
{ new IEqualityComparer<FSharpSymbolUse> with
34+
member __.Equals (x, y) = x.Symbol.IsEffectivelySameAs y.Symbol
35+
member __.GetHashCode x = x.Symbol.GetHashCode() }
36+
37+
let countSymbolsUses (symbolsUses: FSharpSymbolUse[]) =
38+
let result = Dictionary<FSharpSymbolUse, int>(symbolUseComparer)
39+
40+
for symbolUse in symbolsUses do
41+
match result.TryGetValue symbolUse with
42+
| true, count -> result.[symbolUse] <- count + 1
43+
| _ -> result.[symbolUse] <- 1
44+
result
45+
46+
let getSingleDeclarations (symbolsUses: FSharpSymbolUse[]) (isScript: bool) =
47+
let declarations =
48+
countSymbolsUses symbolsUses
49+
|> Seq.choose (fun (KeyValue(symbolUse, count)) ->
50+
match symbolUse.Symbol with
51+
// Determining that a record, DU or module is used anywhere requires inspecting all their enclosed entities (fields, cases and func / vals)
52+
// for usages, which is too expensive to do. Hence we never gray them out.
53+
| :? FSharpEntity as e when e.IsFSharpRecord || e.IsFSharpUnion || e.IsInterface || e.IsFSharpModule || e.IsClass -> None
54+
// FCS returns inconsistent results for override members; we're skipping these symbols.
55+
| :? FSharpMemberOrFunctionOrValue as f when
56+
f.IsOverrideOrExplicitInterfaceImplementation ||
57+
f.IsConstructorThisValue ||
58+
f.IsBaseValue ||
59+
f.IsConstructor -> None
60+
// Usage of DU case parameters does not give any meaningful feedback; we never gray them out.
61+
| :? FSharpParameter when symbolUse.IsFromDefinition -> None
62+
| _ when count = 1 && symbolUse.IsFromDefinition && (isScript || symbolUse.IsPrivateToFile) -> Some symbolUse
63+
| _ -> None)
64+
HashSet(declarations, symbolUseComparer)
65+
66+
override __.SupportedDiagnostics = ImmutableArray.Create Descriptor
67+
68+
override this.AnalyzeSyntaxAsync(_, _) = Task.FromResult ImmutableArray<Diagnostic>.Empty
69+
70+
override this.AnalyzeSemanticsAsync(document, cancellationToken) =
71+
asyncMaybe {
72+
match getProjectInfoManager(document).TryGetOptionsForEditingDocumentOrProject(document) with
73+
| Some options ->
74+
let! sourceText = document.GetTextAsync()
75+
let checker = getChecker document
76+
let! _, _, checkResults = checker.ParseAndCheckDocument(document, options, sourceText = sourceText, allowStaleResults = true)
77+
let! allSymbolUsesInFile = checkResults.GetAllUsesOfAllSymbolsInFile() |> liftAsync
78+
let unusedDeclarations = getSingleDeclarations allSymbolUsesInFile (isScriptFile document.FilePath)
79+
return
80+
unusedDeclarations
81+
|> Seq.filter (fun symbolUse -> not (symbolUse.Symbol.DisplayName.StartsWith "_"))
82+
|> Seq.map (fun symbolUse -> Diagnostic.Create(Descriptor, RoslynHelpers.RangeToLocation(symbolUse.RangeAlternate, sourceText, document.FilePath)))
83+
|> Seq.toImmutableArray
84+
| None -> return ImmutableArray.Empty
85+
}
86+
|> Async.map (Option.defaultValue ImmutableArray.Empty)
87+
|> RoslynHelpers.StartAsyncAsTask cancellationToken
88+
89+
interface IBuiltInAnalyzer with
90+
member __.OpenFileOnly _ = true
91+
member __.GetAnalyzerCategory() = DiagnosticAnalyzerCategory.SemanticDocumentAnalysis

vsintegration/src/FSharp.Editor/FSharp.Editor.fsproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
<Compile Include="Diagnostics\DocumentDiagnosticAnalyzer.fs" />
5959
<Compile Include="Diagnostics\ProjectDiagnosticAnalyzer.fs" />
6060
<Compile Include="Diagnostics\SimplifyNameDiagnosticAnalyzer.fs" />
61+
<Compile Include="Diagnostics\UnusedDeclarationsAnalyzer.fs" />
6162
<Compile Include="Diagnostics\UnusedOpensDiagnosticAnalyzer.fs" />
6263
<Compile Include="Completion\CompletionUtils.fs" />
6364
<Compile Include="Completion\CompletionProvider.fs" />

vsintegration/src/FSharp.Editor/FSharp.Editor.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,4 +174,7 @@
174174
<data name="6010" xml:space="preserve">
175175
<value>Code Fixes</value>
176176
</data>
177+
<data name="TheValueIsUnused" xml:space="preserve">
178+
<value>The value is unused</value>
179+
</data>
177180
</root>

vsintegration/src/FSharp.Editor/srFSharp.Editor.fs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ module SR =
2929
let FSharpPrintfFormatClassificationType = lazy (GetString "FSharpPrintfFormatClassificationType")
3030
let FSharpPropertiesClassificationType = lazy (GetString "FSharpPropertiesClassificationType")
3131
let FSharpDisposablesClassificationType = lazy (GetString "FSharpDisposablesClassificationType")
32+
let TheValueIsUnused = lazy (GetString "TheValueIsUnused")
3233
let RemoveUnusedOpens = lazy (GetString "RemoveUnusedOpens")
3334
let UnusedOpens = lazy (GetString "UnusedOpens")
3435
let AddProjectReference = lazy (GetString "AddProjectReference")

0 commit comments

Comments
 (0)