Skip to content

Commit

Permalink
Update type abbrev mismatch (#1244)
Browse files Browse the repository at this point in the history
* Got one example working.

* Add unit tests

* Extract re-usable logic into partial active patterns.

* No focus tests
  • Loading branch information
nojaf authored Mar 15, 2024
1 parent 8162411 commit fe5cd3c
Show file tree
Hide file tree
Showing 8 changed files with 271 additions and 108 deletions.
26 changes: 26 additions & 0 deletions src/FsAutoComplete.Core/TypedAstPatterns.fs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module FsAutoComplete.Patterns

open FSharp.Compiler.CodeAnalysis
open FSharp.Compiler.Symbols
open FSharp.Compiler.Text


/// Active patterns over `FSharpSymbolUse`.
Expand Down Expand Up @@ -253,6 +254,31 @@ module SymbolUse =
| Entity(entity, _) when entity.IsAttributeType -> Some entity
| _ -> None

let trySignatureLocation (signatureLocation: range option) =
match signatureLocation with
| None -> None
| Some signatureLocation ->
if not (isSignatureFile signatureLocation.FileName) then
None
else
Some signatureLocation

let (|IsInSignature|_|) (symbolUse: FSharpSymbolUse) = trySignatureLocation symbolUse.Symbol.SignatureLocation

let (|IsParentInSignature|_|) (symbolUse: FSharpSymbolUse) =
match trySignatureLocation symbolUse.Symbol.SignatureLocation with
// We are interested in the scenarios when the current symbol is not in a signature file but the parent is.
| Some _ -> None
| None ->
let parentOpt =
match symbolUse.Symbol with
| :? FSharpEntity as entity -> entity.DeclaringEntity
| :? FSharpMemberOrFunctionOrValue as mfv -> mfv.DeclaringEntity
| _ -> None

parentOpt
|> Option.bind (fun parentEntity -> trySignatureLocation parentEntity.SignatureLocation)

/// Active patterns over `FSharpSymbol`.
[<AutoOpen>]
module SymbolPatterns =
Expand Down
7 changes: 7 additions & 0 deletions src/FsAutoComplete.Core/TypedAstPatterns.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module FsAutoComplete.Patterns

open FSharp.Compiler.CodeAnalysis
open FSharp.Compiler.Symbols
open FSharp.Compiler.Text

/// Active patterns over `FSharpSymbolUse`.
module SymbolUse =
Expand Down Expand Up @@ -36,6 +37,12 @@ module SymbolUse =
val (|ValueType|_|): (FSharpSymbolUse -> FSharpEntity option)
val (|ComputationExpression|_|): symbol: FSharpSymbolUse -> FSharpSymbolUse option
val (|Attribute|_|): (FSharpSymbolUse -> FSharpEntity option)
/// Check if the symbolUse.Symbol.SignatureLocation is in an actual signature file.
val (|IsInSignature|_|): symbolUse: FSharpSymbolUse -> range option
/// Check if the symbolUse.Symbol is not in an actual signature file
/// but the declaring entity (in case the symbol is FSharpEntity or FSharpMemberOrFunctionOrValue)
/// is located inside an actual signature file.
val (|IsParentInSignature|_|): symbolUse: FSharpSymbolUse -> range option

/// Active patterns over `FSharpSymbol`.
[<AutoOpen>]
Expand Down
189 changes: 83 additions & 106 deletions src/FsAutoComplete/CodeFixes/AddTypeAliasToSignatureFile.fs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
module FsAutoComplete.CodeFix.AddTypeAliasToSignatureFile

open System
open FSharp.Compiler.Symbols
open FSharp.Compiler.Syntax
open FSharp.Compiler.Text
open FSharp.Compiler.CodeAnalysis
Expand All @@ -10,6 +9,7 @@ open Ionide.LanguageServerProtocol.Types
open FsAutoComplete.CodeFix.Types
open FsAutoComplete
open FsAutoComplete.LspHelpers
open FsAutoComplete.Patterns.SymbolUse

let mkLongIdRange (lid: LongIdent) = lid |> List.map (fun ident -> ident.idRange) |> List.reduce Range.unionRanges

Expand Down Expand Up @@ -90,111 +90,88 @@ let fix
| Some(typeName, mTypeDefn) ->

match parseAndCheckResults.TryGetSymbolUseFromIdent sourceText typeName with
| None -> return []
| Some typeSymbolUse ->

match typeSymbolUse.Symbol with
| :? FSharpEntity as entity ->
let isPartOfSignature =
match entity.SignatureLocation with
| None -> false
| Some sigLocation -> Utils.isSignatureFile sigLocation.FileName

if isPartOfSignature then
return []
else

let implFilePath = codeActionParams.TextDocument.GetFilePath()
let sigFilePath = $"%s{implFilePath}i"
let sigFileName = Utils.normalizePath sigFilePath

let sigTextDocumentIdentifier: TextDocumentIdentifier =
{ Uri = $"%s{codeActionParams.TextDocument.Uri}i" }

let! (sigParseAndCheckResults: ParseAndCheckResults, _sigLine: string, sigSourceText: IFSACSourceText) =
getParseResultsForFile sigFileName (Position.mkPos 1 0)

let parentSigLocation =
entity.DeclaringEntity
|> Option.bind (fun parentEntity ->
match parentEntity.SignatureLocation with
| Some sigLocation when Utils.isSignatureFile sigLocation.FileName -> Some sigLocation
| _ -> None)

match parentSigLocation with
| None -> return []
| Some parentSigLocation ->

// Find a good location to insert the type alias
let insertText =
(parentSigLocation.Start, sigParseAndCheckResults.GetParseResults.ParseTree)
||> ParsedInput.tryPick (fun _path node ->
| Some(IsParentInSignature parentSigLocation) ->

let implFilePath = codeActionParams.TextDocument.GetFilePath()
let sigFilePath = $"%s{implFilePath}i"
let sigFileName = Utils.normalizePath sigFilePath

let sigTextDocumentIdentifier: TextDocumentIdentifier =
{ Uri = $"%s{codeActionParams.TextDocument.Uri}i" }

let! (sigParseAndCheckResults: ParseAndCheckResults, _sigLine: string, sigSourceText: IFSACSourceText) =
getParseResultsForFile sigFileName (Position.mkPos 1 0)

// Find a good location to insert the type alias
let insertText =
(parentSigLocation.Start, sigParseAndCheckResults.GetParseResults.ParseTree)
||> ParsedInput.tryPick (fun _path node ->
match node with
| SyntaxNode.SynModuleOrNamespaceSig(SynModuleOrNamespaceSig(longId = longId; decls = decls))
| SyntaxNode.SynModuleSigDecl(SynModuleSigDecl.NestedModule(
moduleInfo = SynComponentInfo(longId = longId); moduleDecls = decls)) ->
let mSigName = mkLongIdRange longId

// `parentSigLocation` will only contain the single identifier in case a module is prefixed with a namespace.
if not (Range.rangeContainsRange mSigName parentSigLocation) then
None
else

let aliasText =
let text = sourceText.GetSubTextFromRange mTypeDefn

if not (text.StartsWith("and", StringComparison.Ordinal)) then
text
else
String.Concat("type", text.Substring 3)

match decls with
| [] ->
match node with
| SyntaxNode.SynModuleOrNamespaceSig(SynModuleOrNamespaceSig(longId = longId; decls = decls))
| SyntaxNode.SynModuleOrNamespaceSig nm ->
Some(nm.Range.EndRange, String.Concat("\n\n", aliasText))

| SyntaxNode.SynModuleSigDecl(SynModuleSigDecl.NestedModule(
moduleInfo = SynComponentInfo(longId = longId); moduleDecls = decls)) ->
let mSigName = mkLongIdRange longId

// `parentSigLocation` will only contain the single identifier in case a module is prefixed with a namespace.
if not (Range.rangeContainsRange mSigName parentSigLocation) then
None
else

let aliasText =
let text = sourceText.GetSubTextFromRange mTypeDefn

if not (text.StartsWith("and", StringComparison.Ordinal)) then
text
else
String.Concat("type", text.Substring 3)

match decls with
| [] ->
match node with
| SyntaxNode.SynModuleOrNamespaceSig nm ->
Some(nm.Range.EndRange, String.Concat("\n\n", aliasText))

| SyntaxNode.SynModuleSigDecl(SynModuleSigDecl.NestedModule(
range = mNested
trivia = { ModuleKeyword = Some mModule
EqualsRange = Some mEquals })) ->
let moduleEqualsText =
sigSourceText.GetSubTextFromRange(Range.unionRanges mModule mEquals)
// Can this grabbed from configuration?
let indent = " "

Some(mNested, String.Concat(moduleEqualsText, "\n", indent, aliasText))
| _ -> None
| AllOpenOrHashDirective mLastDecl -> Some(mLastDecl, String.Concat("\n\n", aliasText))
| decls ->

decls
// Skip open statements
|> List.tryFind (function
| SynModuleSigDecl.Open _
| SynModuleSigDecl.HashDirective _ -> false
| _ -> true)
|> Option.map (fun mdl ->
let offset =
if mdl.Range.StartColumn = 0 then
String.Empty
else
String.replicate mdl.Range.StartColumn " "

mdl.Range.StartRange, String.Concat(aliasText, "\n\n", offset))
| _ -> None)

match insertText with
| None -> return []
| Some(mInsert, newText) ->

return
[ { SourceDiagnostic = None
Title = title
File = sigTextDocumentIdentifier
Edits =
[| { Range = fcsRangeToLsp mInsert
NewText = newText } |]
Kind = FixKind.Fix } ]
| _ -> return []
range = mNested
trivia = { ModuleKeyword = Some mModule
EqualsRange = Some mEquals })) ->
let moduleEqualsText =
sigSourceText.GetSubTextFromRange(Range.unionRanges mModule mEquals)
// Can this grabbed from configuration?
let indent = " "

Some(mNested, String.Concat(moduleEqualsText, "\n", indent, aliasText))
| _ -> None
| AllOpenOrHashDirective mLastDecl -> Some(mLastDecl, String.Concat("\n\n", aliasText))
| decls ->

decls
// Skip open statements
|> List.tryFind (function
| SynModuleSigDecl.Open _
| SynModuleSigDecl.HashDirective _ -> false
| _ -> true)
|> Option.map (fun mdl ->
let offset =
if mdl.Range.StartColumn = 0 then
String.Empty
else
String.replicate mdl.Range.StartColumn " "

mdl.Range.StartRange, String.Concat(aliasText, "\n\n", offset))
| _ -> None)

match insertText with
| None -> return []
| Some(mInsert, newText) ->

return
[ { SourceDiagnostic = None
Title = title
File = sigTextDocumentIdentifier
Edits =
[| { Range = fcsRangeToLsp mInsert
NewText = newText } |]
Kind = FixKind.Fix } ]
| _ -> return []
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
module FsAutoComplete.CodeFix.UpdateTypeAbbreviationInSignatureFile

open FSharp.Compiler.Symbols
open FSharp.Compiler.Syntax
open FSharp.Compiler.Text
open FsToolkit.ErrorHandling
open Ionide.LanguageServerProtocol.Types
open FsAutoComplete.CodeFix.Types
open FsAutoComplete
open FsAutoComplete.LspHelpers
open FsAutoComplete.Patterns.SymbolUse

let title = "Update type abbreviation in signature file"

let fix (getParseResultsForFile: GetParseResultsForFile) : CodeFix =
Run.ifDiagnosticByCode (Set.ofList [ "318" ]) (fun diagnostic codeActionParams ->
asyncResult {
let implFilePath = codeActionParams.TextDocument.GetFilePath()
let implFileName = Utils.normalizePath implFilePath

let! (implParseAndCheckResults: ParseAndCheckResults, _implLine: string, implSourceText: IFSACSourceText) =
getParseResultsForFile implFileName (protocolPosToPos diagnostic.Range.Start)

let mDiag =
protocolRangeToRange implParseAndCheckResults.GetParseResults.FileName diagnostic.Range

let implTypeName =
(mDiag.Start, implParseAndCheckResults.GetParseResults.ParseTree)
||> ParsedInput.tryPick (fun _ node ->
match node with
| SyntaxNode.SynTypeDefn(SynTypeDefn(
typeInfo = SynComponentInfo(longId = [ typeIdent ])
typeRepr = SynTypeDefnRepr.Simple(simpleRepr = SynTypeDefnSimpleRepr.TypeAbbrev _; range = mBody))) when
Range.equals typeIdent.idRange mDiag
->
Some(typeIdent, mBody)
| _ -> None)

match implTypeName with
| None -> return []
| Some(typeName, mImplBody) ->
match implParseAndCheckResults.TryGetSymbolUseFromIdent implSourceText typeName with
| Some(IsInSignature signatureLocation) ->
let sigFilePath = $"%s{implFilePath}i"
let sigFileName = Utils.normalizePath sigFilePath

let sigTextDocumentIdentifier: TextDocumentIdentifier =
{ Uri = $"%s{codeActionParams.TextDocument.Uri}i" }

let! (sigParseAndCheckResults: ParseAndCheckResults, _sigLine: string, _sigSourceText: IFSACSourceText) =
getParseResultsForFile sigFileName (Position.mkPos 1 0)

let mSigTypeAbbrev =
(signatureLocation.Start, sigParseAndCheckResults.GetParseResults.ParseTree)
||> ParsedInput.tryPick (fun _path node ->
match node with
| SyntaxNode.SynTypeDefnSig(SynTypeDefnSig(
typeInfo = SynComponentInfo(longId = [ typeIdent ])
typeRepr = SynTypeDefnSigRepr.Simple(repr = SynTypeDefnSimpleRepr.TypeAbbrev _; range = m))) when
Range.equals typeIdent.idRange signatureLocation
->
Some m
| _ -> None)

match mSigTypeAbbrev with
| None -> return []
| Some mSigTypeAbbrev ->
let newText = implSourceText.GetSubTextFromRange mImplBody

return
[ { SourceDiagnostic = None
Title = title
File = sigTextDocumentIdentifier
Edits =
[| { Range = fcsRangeToLsp mSigTypeAbbrev
NewText = newText } |]
Kind = FixKind.Fix } ]
| _ -> return []
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module FsAutoComplete.CodeFix.UpdateTypeAbbreviationInSignatureFile

open FsAutoComplete.CodeFix.Types

val title: string
val fix: getParseResultsForFile: GetParseResultsForFile -> CodeFix
3 changes: 2 additions & 1 deletion src/FsAutoComplete/LspServers/AdaptiveServerState.fs
Original file line number Diff line number Diff line change
Expand Up @@ -1903,7 +1903,8 @@ type AdaptiveState(lspClient: FSharpLspClient, sourceTextFactory: ISourceTextFac
AdjustConstant.fix tryGetParseAndCheckResultsForFile
UpdateValueInSignatureFile.fix tryGetParseAndCheckResultsForFile
RemoveUnnecessaryParentheses.fix forceFindSourceText
AddTypeAliasToSignatureFile.fix forceGetFSharpProjectOptions tryGetParseAndCheckResultsForFile |])
AddTypeAliasToSignatureFile.fix forceGetFSharpProjectOptions tryGetParseAndCheckResultsForFile
UpdateTypeAbbreviationInSignatureFile.fix tryGetParseAndCheckResultsForFile |])

let forgetDocument (uri: DocumentUri) =
async {
Expand Down
3 changes: 2 additions & 1 deletion test/FsAutoComplete.Tests.Lsp/CodeFixTests/Tests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -3433,4 +3433,5 @@ let tests textFactory state =
removePatternArgumentTests state
UpdateValueInSignatureFileTests.tests state
removeUnnecessaryParenthesesTests state
AddTypeAliasToSignatureFileTests.tests state ]
AddTypeAliasToSignatureFileTests.tests state
UpdateTypeAbbreviationInSignatureFileTests.tests state ]
Loading

0 comments on commit fe5cd3c

Please sign in to comment.