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

Codefix to add private access modifier #1089

Merged
merged 15 commits into from
Apr 1, 2023
235 changes: 235 additions & 0 deletions src/FsAutoComplete/CodeFixes/AddPrivateAccessModifier.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
module FsAutoComplete.CodeFix.AddPrivateAccessModifier

open FsToolkit.ErrorHandling
open FsAutoComplete.CodeFix.Types
open Ionide.LanguageServerProtocol.Types
open FsAutoComplete
open FsAutoComplete.LspHelpers
open FSharp.Compiler.Syntax
open FSharp.Compiler.SyntaxTrivia
open FSharp.Compiler.Text.Range

let title = "Add private access modifier"

type SymbolUseWorkspace =
bool
-> bool
-> bool
-> FSharp.Compiler.Text.Position
-> LineStr
-> NamedText
-> ParseAndCheckResults
-> Async<Result<FSharp.Compiler.Symbols.FSharpSymbol *
System.Collections.Generic.IDictionary<FSharp.UMX.string<LocalPath>, FSharp.Compiler.Text.range array>, string>>

type private Placement =
| Before
| After

let private getRangesAndPlacement input pos =

let getEditRangeForModule (attributes: SynAttributes) (moduleKeywordRange: FSharp.Compiler.Text.Range) posLine =
match List.tryLast attributes with
| Some a when a.Range.EndLine = posLine -> a.Range.WithStart a.Range.End
| _ -> moduleKeywordRange.WithStart moduleKeywordRange.End

let longIdentContainsPos (longIdent: LongIdent) (pos: FSharp.Compiler.Text.pos) =
longIdent
|> List.tryFind (fun i -> rangeContainsPos i.idRange pos)
|> Option.isSome

let isLetInsideObjectModel (path: SyntaxVisitorPath) pos =
path
|> List.exists (function
| SyntaxNode.SynTypeDefn(SynTypeDefn(typeRepr = SynTypeDefnRepr.ObjectModel(_, members, _))) ->
members
|> List.exists (fun m ->
match m with
| SynMemberDefn.LetBindings(range = range) when rangeContainsPos range pos -> true
| _ -> false)
| _ -> false)

let tryGetDeclContainingRange (path: SyntaxVisitorPath) pos =
let skip =
match path with
| SyntaxNode.SynTypeDefn(SynTypeDefn(typeRepr = SynTypeDefnRepr.ObjectModel _)) :: _ -> 0 // keep containing range of ctor decl to class range
| _ -> 1

path
|> Seq.skip skip
|> Seq.tryPick (fun p ->
match p with
| SyntaxNode.SynTypeDefn m when rangeContainsPos m.Range pos -> Some m.Range
| SyntaxNode.SynModule(SynModuleDecl.NestedModule(range = r)) when rangeContainsPos r pos -> Some r
| SyntaxNode.SynModuleOrNamespace m when rangeContainsPos m.Range pos -> Some m.Range
| _ -> None)

let rec findNested path decls =
decls
|> List.tryPick (fun d ->
match d with
// Nested Module
| SynModuleDecl.NestedModule(
moduleInfo = SynComponentInfo(attributes = attributes; longId = longId; accessibility = None)
trivia = { ModuleKeyword = Some r }) as m when longIdentContainsPos longId pos ->
let editRange = getEditRangeForModule attributes r pos.Line
let path = (SyntaxNode.SynModule m) :: path

match tryGetDeclContainingRange path pos with
| Some r -> Some(editRange, r, After)
| _ -> None
| SynModuleDecl.NestedModule(moduleInfo = moduleInfo; decls = decls) as m ->
let path = (SyntaxNode.SynModule m) :: path

match moduleInfo with
| _ -> findNested path decls
| SynModuleDecl.Types(typeDefns = typeDefns) as t ->
let path = (SyntaxNode.SynModule t) :: path

typeDefns
|> List.tryPick (fun td ->
match td with
// Class Type
| SynTypeDefn(
typeInfo = SynComponentInfo(longId = longId; accessibility = None; range = r)
typeRepr = SynTypeDefnRepr.ObjectModel _) as t when longIdentContainsPos longId pos ->
let editRange = r.WithEnd r.Start
let path = SyntaxNode.SynTypeDefn t :: path

match tryGetDeclContainingRange path pos with
| Some r -> Some(editRange, r, Before)
| _ -> None
// AutoProperty
| SynTypeDefn(typeRepr = SynTypeDefnRepr.ObjectModel(_, members, _)) as t ->
let path = SyntaxNode.SynTypeDefn t :: path

members
|> List.tryPick (fun m ->
match m with
| SynMemberDefn.AutoProperty(accessibility = None; ident = ident; trivia = trivia) as a when
rangeContainsPos ident.idRange pos
->
let editRange =
trivia.LeadingKeyword.Range.WithStart trivia.LeadingKeyword.Range.End

let path = SyntaxNode.SynMemberDefn a :: path

match tryGetDeclContainingRange path pos with
| Some r -> Some(editRange, r, After)
| _ -> None
| _ -> None)
// Type Abbreviation
| SynTypeDefn(
typeInfo = SynComponentInfo(accessibility = None; range = r)
typeRepr = SynTypeDefnRepr.Simple(simpleRepr = SynTypeDefnSimpleRepr.TypeAbbrev _)) as t when
rangeContainsPos r pos
->
let editRange = r.WithEnd r.Start
let path = SyntaxNode.SynTypeDefn t :: path

match tryGetDeclContainingRange path pos with
| Some r -> Some(editRange, r, Before)
| _ -> None
| _ -> None)
| _ -> None)

let visitor =
{ new SyntaxVisitorBase<_>() with
member _.VisitBinding(path, _, synBinding) =
match synBinding with
// explicit Ctor
| SynBinding(valData = SynValData(memberFlags = Some({ MemberKind = SynMemberKind.Constructor }))) -> None
// let bindings, members
| SynBinding(headPat = headPat; kind = SynBindingKind.Normal) as s when
rangeContainsPos s.RangeOfHeadPattern pos
->
if isLetInsideObjectModel path pos then
None
else
match headPat with
| SynPat.LongIdent(longDotId = longDotId; accessibility = None) ->
let posValidInSynLongIdent =
longDotId.LongIdent
|> List.skip (if longDotId.LongIdent.Length > 1 then 1 else 0)
|> List.exists (fun i -> rangeContainsPos i.idRange pos)

if not posValidInSynLongIdent then
None
else
let editRange = s.RangeOfHeadPattern.WithEnd s.RangeOfHeadPattern.Start

match tryGetDeclContainingRange path pos with
| Some r -> Some(editRange, r, Before)
| _ -> None
| SynPat.Named(accessibility = None; isThisVal = false) ->
let editRange = s.RangeOfHeadPattern.WithEnd s.RangeOfHeadPattern.Start

match tryGetDeclContainingRange path pos with
| Some r -> Some(editRange, r, Before)
| _ -> None
| _ -> None
| _ -> None

member _.VisitModuleOrNamespace(path, synModuleOrNamespace) =
match synModuleOrNamespace with
| SynModuleOrNamespace(
longId = longId
attribs = attribs
accessibility = None
trivia = { LeadingKeyword = SynModuleOrNamespaceLeadingKeyword.Module r }) as mOrN when
longIdentContainsPos longId pos
->
let editRange = getEditRangeForModule attribs r pos.Line

if path.Length = 0 then // Top level module
Some(editRange, mOrN.Range, After)
else
match tryGetDeclContainingRange path pos with
| Some r -> Some(editRange, r, After)
| _ -> None
| SynModuleOrNamespace(decls = decls) as mOrN ->
let path = SyntaxNode.SynModuleOrNamespace mOrN :: path
findNested path decls }

SyntaxTraversal.Traverse(pos, input, visitor)

let fix (getParseResultsForFile: GetParseResultsForFile) (symbolUseWorkspace: SymbolUseWorkspace) : CodeFix =
fun codeActionParams ->
asyncResult {
let filePath = codeActionParams.TextDocument.GetFilePath() |> Utils.normalizePath
let fcsPos = protocolPosToPos codeActionParams.Range.Start
let! (parseAndCheck, lineStr, sourceText) = getParseResultsForFile filePath fcsPos
let rangesAndPlacement = getRangesAndPlacement parseAndCheck.GetAST fcsPos

match rangesAndPlacement with
| Some(editRange, declRange, placement) ->

let! (_, uses) = symbolUseWorkspace false true true fcsPos lineStr sourceText parseAndCheck
let useRanges = uses.Values |> Array.concat

let usedOutsideOfDecl =
useRanges
|> Array.exists (fun usingRange ->
usingRange.FileName <> editRange.FileName
|| not (rangeContainsRange declRange usingRange))

if usedOutsideOfDecl then
return []
else
let text =
match placement with
| Before -> "private "
| After -> " private"

let e =
{ Range = fcsRangeToLsp editRange
NewText = text }

return
[ { Edits = [| e |]
File = codeActionParams.TextDocument
Title = title
SourceDiagnostic = None
Kind = FixKind.Refactor } ]
| _ -> return []
}
5 changes: 5 additions & 0 deletions src/FsAutoComplete/LspHelpers.fs
Original file line number Diff line number Diff line change
Expand Up @@ -638,6 +638,7 @@ type FSharpConfigDto =
InterfaceStubGeneration: bool option
InterfaceStubGenerationObjectIdentifier: string option
InterfaceStubGenerationMethodBody: string option
AddPrivateAccessModifier: bool option
UnusedOpensAnalyzer: bool option
UnusedDeclarationsAnalyzer: bool option
SimplifyNameAnalyzer: bool option
Expand Down Expand Up @@ -756,6 +757,7 @@ type FSharpConfig =
InterfaceStubGeneration: bool
InterfaceStubGenerationObjectIdentifier: string
InterfaceStubGenerationMethodBody: string
AddPrivateAccessModifier: bool
UnusedOpensAnalyzer: bool
UnusedDeclarationsAnalyzer: bool
SimplifyNameAnalyzer: bool
Expand Down Expand Up @@ -797,6 +799,7 @@ type FSharpConfig =
InterfaceStubGeneration = false
InterfaceStubGenerationObjectIdentifier = "this"
InterfaceStubGenerationMethodBody = "failwith \"Not Implemented\""
AddPrivateAccessModifier = false
UnusedOpensAnalyzer = false
UnusedDeclarationsAnalyzer = false
SimplifyNameAnalyzer = false
Expand Down Expand Up @@ -836,6 +839,7 @@ type FSharpConfig =
InterfaceStubGenerationObjectIdentifier = defaultArg dto.InterfaceStubGenerationObjectIdentifier "this"
InterfaceStubGenerationMethodBody =
defaultArg dto.InterfaceStubGenerationMethodBody "failwith \"Not Implemented\""
AddPrivateAccessModifier = defaultArg dto.AddPrivateAccessModifier false
UnusedOpensAnalyzer = defaultArg dto.UnusedOpensAnalyzer false
UnusedDeclarationsAnalyzer = defaultArg dto.UnusedDeclarationsAnalyzer false
SimplifyNameAnalyzer = defaultArg dto.SimplifyNameAnalyzer false
Expand Down Expand Up @@ -926,6 +930,7 @@ type FSharpConfig =
defaultArg dto.InterfaceStubGenerationObjectIdentifier x.InterfaceStubGenerationObjectIdentifier
InterfaceStubGenerationMethodBody =
defaultArg dto.InterfaceStubGenerationMethodBody x.InterfaceStubGenerationMethodBody
AddPrivateAccessModifier = defaultArg dto.AddPrivateAccessModifier x.AddPrivateAccessModifier
UnusedOpensAnalyzer = defaultArg dto.UnusedOpensAnalyzer x.UnusedOpensAnalyzer
UnusedDeclarationsAnalyzer = defaultArg dto.UnusedDeclarationsAnalyzer x.UnusedDeclarationsAnalyzer
SimplifyNameAnalyzer = defaultArg dto.SimplifyNameAnalyzer x.SimplifyNameAnalyzer
Expand Down
Loading