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

Update type abbrev mismatch #1244

Merged
merged 4 commits into from
Mar 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading