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 AttributeApplication completion context for unfinished attributes #4126

Merged
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
60 changes: 48 additions & 12 deletions src/fsharp/service/ServiceUntypedParse.fs
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,8 @@ type EntityKind =
override x.ToString() = sprintf "%A" x

module UntypedParseImpl =
open System.Text.RegularExpressions
open Microsoft.FSharp.Compiler.PrettyNaming

let emptyStringSet = HashSet<string>()

Expand Down Expand Up @@ -932,20 +934,13 @@ module UntypedParseImpl =
| ParsedInput.ImplFile input -> walkImplFileInput input

type internal TS = AstTraversal.TraverseStep
/// Matches the most nested [< and >] pair.
let insideAttributeApplicationRegex = Regex(@"(?<=\[\<)(?<attribute>(.*?))(?=\>\])", RegexOptions.Compiled ||| RegexOptions.ExplicitCapture)

/// Try to determine completion context for the given pair (row, columns)
let TryGetCompletionContext (pos, untypedParseOpt: FSharpParseFileResults option, lineStr: string) : CompletionContext option =
let parsedInputOpt =
match untypedParseOpt with
| Some upi -> upi.ParseTree
| None -> None

match parsedInputOpt with
| None -> None
| Some pt ->
let TryGetCompletionContext (pos, parsedInput: ParsedInput, lineStr: string) : CompletionContext option =


match GetEntityKind(pos, pt) with
match GetEntityKind(pos, parsedInput) with
| Some EntityKind.Attribute -> Some CompletionContext.AttributeApplication
| _ ->

Expand Down Expand Up @@ -1282,7 +1277,48 @@ module UntypedParseImpl =
| _ -> defaultTraverse ty
}

AstTraversal.Traverse(pos, pt, walker)
AstTraversal.Traverse(pos, parsedInput, walker)
// Uncompleted attribute applications are not presented in the AST in any way. So, we have to parse source string.
|> Option.orElseWith (fun _ ->
let cutLeadingAttributes (str: string) =
// cut off leading attributes, i.e. we cut "[<A1; A2; >]" to " >]"
match str.LastIndexOf ';' with
| -1 -> str
| idx when idx < str.Length -> str.[idx + 1..].TrimStart()
| _ -> ""

let isLongIdent = Seq.forall (fun c -> IsIdentifierPartCharacter c || c = '.' || c = ':') // ':' may occur in "[<type:AnAttribute>]"

// match the most nested paired [< and >] first
let matches =
insideAttributeApplicationRegex.Matches(lineStr)
|> Seq.cast<Match>
|> Seq.filter (fun m -> m.Index <= pos.Column && m.Index + m.Length >= pos.Column)
|> Seq.toArray

if not (Array.isEmpty matches) then
matches
|> Seq.tryPick (fun m ->
let g = m.Groups.["attribute"]
let col = pos.Column - g.Index
if col >= 0 && col < g.Length then
let str = g.Value.Substring(0, col).TrimStart() // cut other rhs attributes
let str = cutLeadingAttributes str
if isLongIdent str then
Some CompletionContext.AttributeApplication
else None
else None)
else
// Paired [< and >] were not found, try to determine that we are after [< without closing >]
match lineStr.LastIndexOf "[<" with
| -1 -> None
| openParenIndex when pos.Column >= openParenIndex + 2 ->
let str = lineStr.[openParenIndex + 2..pos.Column - 1].TrimStart()
let str = cutLeadingAttributes str
if isLongIdent str then
Some CompletionContext.AttributeApplication
else None
| _ -> None)

/// Check if we are at an "open" declaration
let GetFullNameOfSmallestModuleOrNamespaceAtPoint (parsedInput: ParsedInput, pos: pos) =
Expand Down
2 changes: 1 addition & 1 deletion src/fsharp/service/ServiceUntypedParse.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ module public UntypedParseImpl =
val TryFindExpressionASTLeftOfDotLeftOfCursor : pos * ParsedInput option -> (pos * bool) option
val GetRangeOfExprLeftOfDot : pos * ParsedInput option -> range option
val TryFindExpressionIslandInPosition : pos * ParsedInput option -> string option
val TryGetCompletionContext : pos * FSharpParseFileResults option * lineStr: string -> CompletionContext option
val TryGetCompletionContext : pos * ParsedInput * lineStr: string -> CompletionContext option
val GetEntityKind: pos * ParsedInput -> EntityKind option
val GetFullNameOfSmallestModuleOrNamespaceAtPoint : ParsedInput * pos -> string[]

Expand Down
6 changes: 5 additions & 1 deletion src/fsharp/service/service.fs
Original file line number Diff line number Diff line change
Expand Up @@ -780,7 +780,11 @@ type TypeCheckInfo
| otherwise -> otherwise - 1

// Look for a "special" completion context
let completionContext = UntypedParseImpl.TryGetCompletionContext(mkPos line colAtEndOfNamesAndResidue, parseResultsOpt, lineStr)
let completionContext =
parseResultsOpt
|> Option.bind (fun x -> x.ParseTree)
|> Option.bind (fun parseTree -> UntypedParseImpl.TryGetCompletionContext(mkPos line colAtEndOfNamesAndResidue, parseTree, lineStr))

let res =
match completionContext with
// Invalid completion locations
Expand Down
102 changes: 102 additions & 0 deletions tests/service/ServiceUntypedParseTests.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
#if INTERACTIVE
#r "../../Debug/fcs/net45/FSharp.Compiler.Service.dll" // note, run 'build fcs debug' to generate this, this DLL has a public API so can be used from F# Interactive
#r "../../packages/NUnit.3.5.0/lib/net45/nunit.framework.dll"
#load "FsUnit.fs"
#load "Common.fs"
#else
module Tests.Service.ServiceUntypedParseTests
#endif

open System
open System.IO
open System.Text
open NUnit.Framework
open Microsoft.FSharp.Compiler.Range
open Microsoft.FSharp.Compiler.SourceCodeServices
open FSharp.Compiler.Service.Tests.Common
open Tests.Service

let [<Literal>] private Marker = "(* marker *)"

let private (=>) (source: string) (expected: CompletionContext option) =

let lines =
use reader = new StringReader(source)
[| let line = ref (reader.ReadLine())
while not (isNull !line) do
yield !line
line := reader.ReadLine()
if source.EndsWith "\n" then
yield "" |]

let markerPos =
lines
|> Array.mapi (fun i x -> i, x)
|> Array.tryPick (fun (lineIdx, line) ->
match line.IndexOf Marker with
| -1 -> None
| idx -> Some (mkPos (Line.fromZ lineIdx) idx))

match markerPos with
| None -> failwithf "Marker '%s' was not found in the source code" Marker
| Some markerPos ->
match parseSourceCode("C:\\test.fs", source) with
| None -> failwith "No parse tree"
| Some parseTree ->
let actual = UntypedParseImpl.TryGetCompletionContext(markerPos, parseTree, lines.[Line.toZ markerPos.Line])
try Assert.AreEqual(expected, actual)
with e ->
printfn "ParseTree: %A" parseTree
reraise()

module AttributeCompletion =
[<Test>]
let ``at [<|, applied to nothing``() =
"""
[<(* marker *)
"""
=> Some CompletionContext.AttributeApplication

[<TestCase ("[<(* marker *)", true)>]
[<TestCase ("[<AnAttr(* marker *)", true)>]
[<TestCase ("[<type:(* marker *)", true)>]
[<TestCase ("[<type:AnAttr(* marker *)", true)>]
[<TestCase ("[< (* marker *)", true)>]
[<TestCase ("[<AnAttribute;(* marker *)", true)>]
[<TestCase ("[<AnAttribute; (* marker *)", true)>]
[<TestCase ("[<AnAttribute>][<(* marker *)", true)>]
[<TestCase ("[<AnAttribute>][< (* marker *)", true)>]
[<TestCase ("[<AnAttribute((* marker *)", false)>]
[<TestCase ("[<AnAttribute( (* marker *)", false)>]
[<TestCase ("[<AnAttribute (* marker *)", false)>]
[<TestCase ("[<AnAttribute>][<AnAttribute((* marker *)", false)>]
[<TestCase ("[<AnAttribute; AnAttribute((* marker *)", false)>]
let ``incomplete``(lineStr: string, expectAttributeApplicationContext: bool) =
(sprintf """
%s
type T =
{ F: int }
""" lineStr) => (if expectAttributeApplicationContext then Some CompletionContext.AttributeApplication else None)

[<TestCase ("[<(* marker *)>]", true)>]
[<TestCase ("[<AnAttr(* marker *)>]", true)>]
[<TestCase ("[<type:(* marker *)>]", true)>]
[<TestCase ("[<type:AnAttr(* marker *)>]", true)>]
[<TestCase ("[< (* marker *)>]", true)>]
[<TestCase ("[<AnAttribute>][<(* marker *)>]", true)>]
[<TestCase ("[<AnAttribute>][< (* marker *)>]", true)>]
[<TestCase ("[<AnAttribute;(* marker *)>]", true)>]
[<TestCase ("[<AnAttribute; (* marker *) >]", true)>]
[<TestCase ("[<AnAttribute>][<AnAttribute;(* marker *)>]", true)>]
[<TestCase ("[<AnAttribute((* marker *)>]", false)>]
[<TestCase ("[<AnAttribute (* marker *) >]", false)>]
[<TestCase ("[<AnAttribute>][<AnAttribute((* marker *)>]", false)>]
[<TestCase ("[<AnAttribute; AnAttribute((* marker *)>]", false)>]
[<TestCase ("[<AnAttribute; AnAttribute( (* marker *)>]", false)>]
[<TestCase ("[<AnAttribute>][<AnAttribute; AnAttribute((* marker *)>]", false)>]
let ``complete``(lineStr: string, expectAttributeApplicationContext: bool) =
(sprintf """
%s
type T =
{ F: int }
""" lineStr) => (if expectAttributeApplicationContext then Some CompletionContext.AttributeApplication else None)
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,13 @@ type internal FSharpCompletionProvider

if results.Count > 0 && not declarations.IsForType && not declarations.IsError && List.isEmpty partialName.QualifyingIdents then
let lineStr = textLines.[caretLinePos.Line].ToString()
match UntypedParseImpl.TryGetCompletionContext(Pos.fromZ caretLinePos.Line caretLinePos.Character, Some parseResults, lineStr) with

let completionContext =
parseResults.ParseTree
|> Option.bind (fun parseTree ->
UntypedParseImpl.TryGetCompletionContext(Pos.fromZ caretLinePos.Line caretLinePos.Character, parseTree, lineStr))

match completionContext with
| None -> results.AddRange(keywordCompletionItems)
| _ -> ()

Expand Down
35 changes: 17 additions & 18 deletions vsintegration/tests/unittests/Tests.LanguageService.Completion.fs
Original file line number Diff line number Diff line change
Expand Up @@ -182,9 +182,8 @@ type UsingMSBuild() as this =
shouldContain // should contain
shouldNotContain

member public this.AutoCompleteBug70080Helper(programText:string, ?withSuffix: bool) =
let expected = if defaultArg withSuffix false then "AttributeUsageAttribute" else "AttributeUsage"
this.AutoCompleteBug70080HelperHelper(programText, [expected], [])
member public this.AutoCompleteBug70080Helper(programText: string) =
this.AutoCompleteBug70080HelperHelper(programText, ["AttributeUsage"], [])

member private this.testAutoCompleteAdjacentToDot op =
let text = sprintf "System.Console%s" op
Expand Down Expand Up @@ -3546,56 +3545,56 @@ let x = query { for bbbb in abbbbc(*D0*) do
member public this.``Attribute.WhenAttachedToType.Bug70080``() =
this.AutoCompleteBug70080Helper(@"
open System
[<Attr // expect AttributeUsageAttribute from System namespace
type MyAttr() = inherit Attribute()", true)
[<Attr // expect AttributeUsage from System namespace
type MyAttr() = inherit Attribute()")

[<Test>]
member public this.``Attribute.WhenAttachedToNothing.Bug70080``() =
this.AutoCompleteBug70080Helper(@"
open System
[<Attr // expect AttributeUsageAttribute from System namespace
// nothing here", true)
[<Attr // expect AttributeUsage
// nothing here")

[<Test>]
member public this.``Attribute.WhenAttachedToLetInNamespace.Bug70080``() =
this.AutoCompleteBug70080Helper @"
namespace Foo
open System
[<Attr // expect AttributeUsageAttribute from System namespace
[<Attr // expect AttributeUsage from System namespace
let f() = 4"

[<Test>]
member public this.``Attribute.WhenAttachedToTypeInNamespace.Bug70080``() =
this.AutoCompleteBug70080Helper(@"
namespace Foo
open System
[<Attr // expect AttributeUsageAttribute from System namespace
type MyAttr() = inherit Attribute()", true)
[<Attr // expect AttributeUsage from System namespace
type MyAttr() = inherit Attribute()")

[<Test>]
member public this.``Attribute.WhenAttachedToNothingInNamespace.Bug70080``() =
this.AutoCompleteBug70080Helper(@"
namespace Foo
open System
[<Attr // expect AttributeUsageAttribute from System namespace
// nothing here", true)
[<Attr // expect AttributeUsage from System namespace
// nothing here")

[<Test>]
member public this.``Attribute.WhenAttachedToModuleInNamespace.Bug70080``() =
this.AutoCompleteBug70080Helper(@"
namespace Foo
open System
[<Attr // expect AttributeUsageAttribute from System namespace
[<Attr // expect AttributeUsage from System namespace
module Foo =
let x = 42", true)
let x = 42")

[<Test>]
member public this.``Attribute.WhenAttachedToModule.Bug70080``() =
this.AutoCompleteBug70080Helper(@"
open System
[<Attr // expect AttributeUsageAttribute from System namespace
[<Attr // expect AttributeUsage from System namespace
module Foo =
let x = 42", true)
let x = 42")

[<Test>]
member public this.``Identifer.InMatchStatemente.Bug72595``() =
Expand Down Expand Up @@ -5052,7 +5051,7 @@ let x = query { for bbbb in abbbbc(*D0*) do
[<
"""]
"[<"
["AttributeUsageAttribute"]
["AttributeUsage"]
[]

[<Test>]
Expand All @@ -5063,7 +5062,7 @@ let x = query { for bbbb in abbbbc(*D0*) do
[<
"""]
"[<"
["AttributeUsageAttribute"]
["AttributeUsage"]
[]

[<Test>]
Expand Down
3 changes: 3 additions & 0 deletions vsintegration/tests/unittests/VisualFSharp.UnitTests.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,9 @@
<Compile Include="..\..\..\tests\service\AssemblyContentProviderTests.fs">
<Link>AssemblyContentProviderTests.fs</Link>
</Compile>
<Compile Include="..\..\..\tests\service\ServiceUntypedParseTests.fs">
<Link>ServiceUntypedParseTests.fs</Link>
</Compile>
<Compile Include="UnusedOpensTests.fs">
<Link>ServiceAnalysis\UnusedOpensTests.fs</Link>
</Compile>
Expand Down