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

Detect type alias in implementation. #1243

Merged
merged 6 commits into from
Mar 14, 2024
Merged
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
2 changes: 1 addition & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
@@ -22,4 +22,4 @@ indent_size = 2
fsharp_max_array_or_list_width=80
fsharp_max_dot_get_expression_width=80
fsharp_max_function_binding_width=80
fsharp_max_value_binding_width=80
fsharp_max_value_binding_width=80
4 changes: 4 additions & 0 deletions src/FsAutoComplete.Core/ParseAndCheckResults.fs
Original file line number Diff line number Diff line change
@@ -519,6 +519,10 @@ type ParseAndCheckResults
let identIsland = Array.toList identIsland
checkResults.GetSymbolUseAtLocation(pos.Line, colu, lineStr, identIsland)

member x.TryGetSymbolUseFromIdent (sourceText: ISourceText) (ident: Ident) : FSharpSymbolUse option =
let line = sourceText.GetLineString(ident.idRange.EndLine - 1)
x.GetCheckResults.GetSymbolUseAtLocation(ident.idRange.EndLine, ident.idRange.EndColumn, line, [ ident.idText ])

member __.TryGetSymbolUses (pos: Position) (lineStr: LineStr) : FSharpSymbolUse list =
match Lexer.findLongIdents (pos.Column, lineStr) with
| None -> []
2 changes: 2 additions & 0 deletions src/FsAutoComplete.Core/ParseAndCheckResults.fsi
Original file line number Diff line number Diff line change
@@ -67,6 +67,8 @@ type ParseAndCheckResults =

member TryGetSymbolUse: pos: Position -> lineStr: LineStr -> FSharpSymbolUse option

member TryGetSymbolUseFromIdent: ISourceText -> Ident -> FSharpSymbolUse option

member TryGetSymbolUses: pos: Position -> lineStr: LineStr -> FSharpSymbolUse list

member TryGetSymbolUseAndUsages:
200 changes: 200 additions & 0 deletions src/FsAutoComplete/CodeFixes/AddTypeAliasToSignatureFile.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
module FsAutoComplete.CodeFix.AddTypeAliasToSignatureFile

open System
open FSharp.Compiler.Symbols
open FSharp.Compiler.Syntax
open FSharp.Compiler.Text
open FSharp.Compiler.CodeAnalysis
open FsToolkit.ErrorHandling
open Ionide.LanguageServerProtocol.Types
open FsAutoComplete.CodeFix.Types
open FsAutoComplete
open FsAutoComplete.LspHelpers

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

let (|AllOpenOrHashDirective|_|) (decls: SynModuleSigDecl list) : range option =
match decls with
| [] -> None
| decls ->

let allOpenOrHashDirective =
decls
|> List.forall (function
| SynModuleSigDecl.Open _
| SynModuleSigDecl.HashDirective _ -> true
| _ -> false)

if not allOpenOrHashDirective then
None
else
Some (List.last decls).Range.EndRange

type SynTypeDefn with

member x.FullRange =
match x with
| SynTypeDefn(range = m; trivia = { LeadingKeyword = lk }) -> Range.unionRanges lk.Range m

let title = "Add type alias to signature file"

let codeFixForImplementationFileWithSignature
(getProjectOptionsForFile: GetProjectOptionsForFile)
(codeFix: CodeFix)
(codeActionParams: CodeActionParams)
: Async<Result<Fix list, string>> =
async {
let fileName = codeActionParams.TextDocument.GetFilePath() |> Utils.normalizePath
let! project = getProjectOptionsForFile fileName

match project with
| Error _ -> return Ok []
| Ok projectOptions ->

let signatureFile = String.Concat(fileName, "i")
let hasSig = projectOptions.SourceFiles |> Array.contains signatureFile

if not hasSig then
return Ok []
else
return! codeFix codeActionParams
}

let fix
(getProjectOptionsForFile: GetProjectOptionsForFile)
(getParseResultsForFile: GetParseResultsForFile)
: CodeFix =
codeFixForImplementationFileWithSignature getProjectOptionsForFile (fun (codeActionParams: CodeActionParams) ->
asyncResult {
let fileName = codeActionParams.TextDocument.GetFilePath() |> Utils.normalizePath
// The converted LSP start position to an FCS start position.
let fcsPos = protocolPosToPos codeActionParams.Range.Start
// The syntax tree and typed tree, current line and sourceText of the current file.
let! (parseAndCheckResults: ParseAndCheckResults, _line: string, sourceText: IFSACSourceText) =
getParseResultsForFile fileName fcsPos

let typeDefnInfo =
(fcsPos, parseAndCheckResults.GetParseResults.ParseTree)
||> ParsedInput.tryPick (fun _path node ->
match node with
| SyntaxNode.SynTypeDefn(SynTypeDefn(
typeInfo = SynComponentInfo(longId = [ typeIdent ])
typeRepr = SynTypeDefnRepr.Simple(simpleRepr = SynTypeDefnSimpleRepr.TypeAbbrev _)) as tdn) when
(Range.rangeContainsPos tdn.FullRange fcsPos)
->
Some(typeIdent, tdn.FullRange)
| _ -> None)

match typeDefnInfo with
| None -> return []
| 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 ->
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 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 []
})
6 changes: 6 additions & 0 deletions src/FsAutoComplete/CodeFixes/AddTypeAliasToSignatureFile.fsi
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module FsAutoComplete.CodeFix.AddTypeAliasToSignatureFile

open FsAutoComplete.CodeFix.Types

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

let forgetDocument (uri: DocumentUri) =
async {
Loading

Unchanged files with check annotations Beta

<Project Sdk="Microsoft.NET.Sdk">

Check failure on line 1 in test/FsAutoComplete.Tests.Lsp/FsAutoComplete.Tests.Lsp.fsproj

GitHub Actions / Build on windows-latest for 8.0

FSAC.lsp.Ionide WorkspaceLoader.CodeFix-tests.ReplaceWithSuggestion.can change namespace in open

Diagnostic with code 39 should suggest name. There isn't any element which satisfies given assertion <fun:validateDiags@2685-3>. at FsAutoComplete.Tests.CodeFixTests.Tests.validateDiags@2681-2.Invoke(Diagnostic[] diags) in D:\a\FsAutoComplete\FsAutoComplete\test\FsAutoComplete.Tests.Lsp\CodeFixTests\Tests.fs:line 2683 at Utils.CursorbasedTests.CodeFix.checkFixAt@44.Invoke(Unit unitVar) in D:\a\FsAutoComplete\FsAutoComplete\test\FsAutoComplete.Tests.Lsp\Utils\CursorbasedTests.fs:line 45 at Microsoft.FSharp.Control.AsyncPrimitives.CallThenInvoke[T,TResult](AsyncActivation`1 ctxt, TResult result1, FSharpFunc`2 part2) in D:\a\_work\1\s\src\FSharp.Core\async.fs:line 510 at Microsoft.FSharp.Control.Trampoline.Execute(FSharpFunc`2 firstAction) in D:\a\_work\1\s\src\FSharp.Core\async.fs:line 112
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net6.0</TargetFrameworks>