Skip to content

Commit 3b1a8cb

Browse files
authored
Codefix to add private access modifier (#1089)
1 parent 22a45da commit 3b1a8cb

File tree

6 files changed

+801
-90
lines changed

6 files changed

+801
-90
lines changed
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
module FsAutoComplete.CodeFix.AddPrivateAccessModifier
2+
3+
open FsToolkit.ErrorHandling
4+
open FsAutoComplete.CodeFix.Types
5+
open Ionide.LanguageServerProtocol.Types
6+
open FsAutoComplete
7+
open FsAutoComplete.LspHelpers
8+
open FSharp.Compiler.Syntax
9+
open FSharp.Compiler.SyntaxTrivia
10+
open FSharp.Compiler.Text.Range
11+
12+
let title = "Add private access modifier"
13+
14+
type SymbolUseWorkspace =
15+
bool
16+
-> bool
17+
-> bool
18+
-> FSharp.Compiler.Text.Position
19+
-> LineStr
20+
-> NamedText
21+
-> ParseAndCheckResults
22+
-> Async<Result<FSharp.Compiler.Symbols.FSharpSymbol *
23+
System.Collections.Generic.IDictionary<FSharp.UMX.string<LocalPath>, FSharp.Compiler.Text.range array>, string>>
24+
25+
type private Placement =
26+
| Before
27+
| After
28+
29+
let private getRangesAndPlacement input pos =
30+
31+
let getEditRangeForModule (attributes: SynAttributes) (moduleKeywordRange: FSharp.Compiler.Text.Range) posLine =
32+
match List.tryLast attributes with
33+
| Some a when a.Range.EndLine = posLine -> a.Range.WithStart a.Range.End
34+
| _ -> moduleKeywordRange.WithStart moduleKeywordRange.End
35+
36+
let longIdentContainsPos (longIdent: LongIdent) (pos: FSharp.Compiler.Text.pos) =
37+
longIdent
38+
|> List.tryFind (fun i -> rangeContainsPos i.idRange pos)
39+
|> Option.isSome
40+
41+
let isLetInsideObjectModel (path: SyntaxVisitorPath) pos =
42+
path
43+
|> List.exists (function
44+
| SyntaxNode.SynTypeDefn(SynTypeDefn(typeRepr = SynTypeDefnRepr.ObjectModel(_, members, _))) ->
45+
members
46+
|> List.exists (fun m ->
47+
match m with
48+
| SynMemberDefn.LetBindings(range = range) when rangeContainsPos range pos -> true
49+
| _ -> false)
50+
| _ -> false)
51+
52+
let tryGetDeclContainingRange (path: SyntaxVisitorPath) pos =
53+
let skip =
54+
match path with
55+
| SyntaxNode.SynTypeDefn(SynTypeDefn(typeRepr = SynTypeDefnRepr.ObjectModel _)) :: _ -> 0 // keep containing range of ctor decl to class range
56+
| _ -> 1
57+
58+
path
59+
|> Seq.skip skip
60+
|> Seq.tryPick (fun p ->
61+
match p with
62+
| SyntaxNode.SynTypeDefn m when rangeContainsPos m.Range pos -> Some m.Range
63+
| SyntaxNode.SynModule(SynModuleDecl.NestedModule(range = r)) when rangeContainsPos r pos -> Some r
64+
| SyntaxNode.SynModuleOrNamespace m when rangeContainsPos m.Range pos -> Some m.Range
65+
| _ -> None)
66+
67+
let rec findNested path decls =
68+
decls
69+
|> List.tryPick (fun d ->
70+
match d with
71+
// Nested Module
72+
| SynModuleDecl.NestedModule(
73+
moduleInfo = SynComponentInfo(attributes = attributes; longId = longId; accessibility = None)
74+
trivia = { ModuleKeyword = Some r }) as m when longIdentContainsPos longId pos ->
75+
let editRange = getEditRangeForModule attributes r pos.Line
76+
let path = (SyntaxNode.SynModule m) :: path
77+
78+
match tryGetDeclContainingRange path pos with
79+
| Some r -> Some(editRange, r, After)
80+
| _ -> None
81+
| SynModuleDecl.NestedModule(moduleInfo = moduleInfo; decls = decls) as m ->
82+
let path = (SyntaxNode.SynModule m) :: path
83+
84+
match moduleInfo with
85+
| _ -> findNested path decls
86+
| SynModuleDecl.Types(typeDefns = typeDefns) as t ->
87+
let path = (SyntaxNode.SynModule t) :: path
88+
89+
typeDefns
90+
|> List.tryPick (fun td ->
91+
match td with
92+
// Class Type
93+
| SynTypeDefn(
94+
typeInfo = SynComponentInfo(longId = longId; accessibility = None; range = r)
95+
typeRepr = SynTypeDefnRepr.ObjectModel _) as t when longIdentContainsPos longId pos ->
96+
let editRange = r.WithEnd r.Start
97+
let path = SyntaxNode.SynTypeDefn t :: path
98+
99+
match tryGetDeclContainingRange path pos with
100+
| Some r -> Some(editRange, r, Before)
101+
| _ -> None
102+
// AutoProperty
103+
| SynTypeDefn(typeRepr = SynTypeDefnRepr.ObjectModel(_, members, _)) as t ->
104+
let path = SyntaxNode.SynTypeDefn t :: path
105+
106+
members
107+
|> List.tryPick (fun m ->
108+
match m with
109+
| SynMemberDefn.AutoProperty(accessibility = None; ident = ident; trivia = trivia) as a when
110+
rangeContainsPos ident.idRange pos
111+
->
112+
let editRange =
113+
trivia.LeadingKeyword.Range.WithStart trivia.LeadingKeyword.Range.End
114+
115+
let path = SyntaxNode.SynMemberDefn a :: path
116+
117+
match tryGetDeclContainingRange path pos with
118+
| Some r -> Some(editRange, r, After)
119+
| _ -> None
120+
| _ -> None)
121+
// Type Abbreviation
122+
| SynTypeDefn(
123+
typeInfo = SynComponentInfo(accessibility = None; range = r)
124+
typeRepr = SynTypeDefnRepr.Simple(simpleRepr = SynTypeDefnSimpleRepr.TypeAbbrev _)) as t when
125+
rangeContainsPos r pos
126+
->
127+
let editRange = r.WithEnd r.Start
128+
let path = SyntaxNode.SynTypeDefn t :: path
129+
130+
match tryGetDeclContainingRange path pos with
131+
| Some r -> Some(editRange, r, Before)
132+
| _ -> None
133+
| _ -> None)
134+
| _ -> None)
135+
136+
let visitor =
137+
{ new SyntaxVisitorBase<_>() with
138+
member _.VisitBinding(path, _, synBinding) =
139+
match synBinding with
140+
// explicit Ctor
141+
| SynBinding(valData = SynValData(memberFlags = Some({ MemberKind = SynMemberKind.Constructor }))) -> None
142+
// let bindings, members
143+
| SynBinding(headPat = headPat; kind = SynBindingKind.Normal) as s when
144+
rangeContainsPos s.RangeOfHeadPattern pos
145+
->
146+
if isLetInsideObjectModel path pos then
147+
None
148+
else
149+
match headPat with
150+
| SynPat.LongIdent(longDotId = longDotId; accessibility = None) ->
151+
let posValidInSynLongIdent =
152+
longDotId.LongIdent
153+
|> List.skip (if longDotId.LongIdent.Length > 1 then 1 else 0)
154+
|> List.exists (fun i -> rangeContainsPos i.idRange pos)
155+
156+
if not posValidInSynLongIdent then
157+
None
158+
else
159+
let editRange = s.RangeOfHeadPattern.WithEnd s.RangeOfHeadPattern.Start
160+
161+
match tryGetDeclContainingRange path pos with
162+
| Some r -> Some(editRange, r, Before)
163+
| _ -> None
164+
| SynPat.Named(accessibility = None; isThisVal = false) ->
165+
let editRange = s.RangeOfHeadPattern.WithEnd s.RangeOfHeadPattern.Start
166+
167+
match tryGetDeclContainingRange path pos with
168+
| Some r -> Some(editRange, r, Before)
169+
| _ -> None
170+
| _ -> None
171+
| _ -> None
172+
173+
member _.VisitModuleOrNamespace(path, synModuleOrNamespace) =
174+
match synModuleOrNamespace with
175+
| SynModuleOrNamespace(
176+
longId = longId
177+
attribs = attribs
178+
accessibility = None
179+
trivia = { LeadingKeyword = SynModuleOrNamespaceLeadingKeyword.Module r }) as mOrN when
180+
longIdentContainsPos longId pos
181+
->
182+
let editRange = getEditRangeForModule attribs r pos.Line
183+
184+
if path.Length = 0 then // Top level module
185+
Some(editRange, mOrN.Range, After)
186+
else
187+
match tryGetDeclContainingRange path pos with
188+
| Some r -> Some(editRange, r, After)
189+
| _ -> None
190+
| SynModuleOrNamespace(decls = decls) as mOrN ->
191+
let path = SyntaxNode.SynModuleOrNamespace mOrN :: path
192+
findNested path decls }
193+
194+
SyntaxTraversal.Traverse(pos, input, visitor)
195+
196+
let fix (getParseResultsForFile: GetParseResultsForFile) (symbolUseWorkspace: SymbolUseWorkspace) : CodeFix =
197+
fun codeActionParams ->
198+
asyncResult {
199+
let filePath = codeActionParams.TextDocument.GetFilePath() |> Utils.normalizePath
200+
let fcsPos = protocolPosToPos codeActionParams.Range.Start
201+
let! (parseAndCheck, lineStr, sourceText) = getParseResultsForFile filePath fcsPos
202+
let rangesAndPlacement = getRangesAndPlacement parseAndCheck.GetAST fcsPos
203+
204+
match rangesAndPlacement with
205+
| Some(editRange, declRange, placement) ->
206+
207+
let! (_, uses) = symbolUseWorkspace false true true fcsPos lineStr sourceText parseAndCheck
208+
let useRanges = uses.Values |> Array.concat
209+
210+
let usedOutsideOfDecl =
211+
useRanges
212+
|> Array.exists (fun usingRange ->
213+
usingRange.FileName <> editRange.FileName
214+
|| not (rangeContainsRange declRange usingRange))
215+
216+
if usedOutsideOfDecl then
217+
return []
218+
else
219+
let text =
220+
match placement with
221+
| Before -> "private "
222+
| After -> " private"
223+
224+
let e =
225+
{ Range = fcsRangeToLsp editRange
226+
NewText = text }
227+
228+
return
229+
[ { Edits = [| e |]
230+
File = codeActionParams.TextDocument
231+
Title = title
232+
SourceDiagnostic = None
233+
Kind = FixKind.Refactor } ]
234+
| _ -> return []
235+
}

src/FsAutoComplete/LspHelpers.fs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -638,6 +638,7 @@ type FSharpConfigDto =
638638
InterfaceStubGeneration: bool option
639639
InterfaceStubGenerationObjectIdentifier: string option
640640
InterfaceStubGenerationMethodBody: string option
641+
AddPrivateAccessModifier: bool option
641642
UnusedOpensAnalyzer: bool option
642643
UnusedDeclarationsAnalyzer: bool option
643644
SimplifyNameAnalyzer: bool option
@@ -756,6 +757,7 @@ type FSharpConfig =
756757
InterfaceStubGeneration: bool
757758
InterfaceStubGenerationObjectIdentifier: string
758759
InterfaceStubGenerationMethodBody: string
760+
AddPrivateAccessModifier: bool
759761
UnusedOpensAnalyzer: bool
760762
UnusedDeclarationsAnalyzer: bool
761763
SimplifyNameAnalyzer: bool
@@ -797,6 +799,7 @@ type FSharpConfig =
797799
InterfaceStubGeneration = false
798800
InterfaceStubGenerationObjectIdentifier = "this"
799801
InterfaceStubGenerationMethodBody = "failwith \"Not Implemented\""
802+
AddPrivateAccessModifier = false
800803
UnusedOpensAnalyzer = false
801804
UnusedDeclarationsAnalyzer = false
802805
SimplifyNameAnalyzer = false
@@ -836,6 +839,7 @@ type FSharpConfig =
836839
InterfaceStubGenerationObjectIdentifier = defaultArg dto.InterfaceStubGenerationObjectIdentifier "this"
837840
InterfaceStubGenerationMethodBody =
838841
defaultArg dto.InterfaceStubGenerationMethodBody "failwith \"Not Implemented\""
842+
AddPrivateAccessModifier = defaultArg dto.AddPrivateAccessModifier false
839843
UnusedOpensAnalyzer = defaultArg dto.UnusedOpensAnalyzer false
840844
UnusedDeclarationsAnalyzer = defaultArg dto.UnusedDeclarationsAnalyzer false
841845
SimplifyNameAnalyzer = defaultArg dto.SimplifyNameAnalyzer false
@@ -926,6 +930,7 @@ type FSharpConfig =
926930
defaultArg dto.InterfaceStubGenerationObjectIdentifier x.InterfaceStubGenerationObjectIdentifier
927931
InterfaceStubGenerationMethodBody =
928932
defaultArg dto.InterfaceStubGenerationMethodBody x.InterfaceStubGenerationMethodBody
933+
AddPrivateAccessModifier = defaultArg dto.AddPrivateAccessModifier x.AddPrivateAccessModifier
929934
UnusedOpensAnalyzer = defaultArg dto.UnusedOpensAnalyzer x.UnusedOpensAnalyzer
930935
UnusedDeclarationsAnalyzer = defaultArg dto.UnusedDeclarationsAnalyzer x.UnusedDeclarationsAnalyzer
931936
SimplifyNameAnalyzer = defaultArg dto.SimplifyNameAnalyzer x.SimplifyNameAnalyzer

0 commit comments

Comments
 (0)