diff --git a/src/fsharp/CheckFormatStrings.fs b/src/fsharp/CheckFormatStrings.fs index f015bef9b3..34a1673e27 100755 --- a/src/fsharp/CheckFormatStrings.fs +++ b/src/fsharp/CheckFormatStrings.fs @@ -193,27 +193,30 @@ let parseFormatStringInternal (m:range) g (source: string option) fmt bty cty = checkNoZeroFlag c checkNoNumericPrefix c - let collectSpecifierLocation relLine relCol = + let collectSpecifierLocation relLine relCol numStdArgs = + let numArgsForSpecifier = + numStdArgs + (if widthArg then 1 else 0) + (if precisionArg then 1 else 0) match relLine with | 0 -> specifierLocations.Add( - Range.mkFileIndexRange m.FileIndex + (Range.mkFileIndexRange m.FileIndex (Range.mkPos m.StartLine (startCol + offset)) - (Range.mkPos m.StartLine (relCol + offset))) + (Range.mkPos m.StartLine (relCol + offset))), numArgsForSpecifier) | _ -> specifierLocations.Add( - Range.mkFileIndexRange m.FileIndex + (Range.mkFileIndexRange m.FileIndex (Range.mkPos (m.StartLine + relLine) startCol) - (Range.mkPos (m.StartLine + relLine) relCol)) + (Range.mkPos (m.StartLine + relLine) relCol)), numArgsForSpecifier) let ch = fmt.[i] match ch with - | '%' -> + | '%' -> + collectSpecifierLocation relLine relCol 0 parseLoop acc (i+1, relLine, relCol+1) | ('d' | 'i' | 'o' | 'u' | 'x' | 'X') -> if info.precision then failwithf "%s" <| FSComp.SR.forFormatDoesntSupportPrecision(ch.ToString()) - collectSpecifierLocation relLine relCol + collectSpecifierLocation relLine relCol 1 parseLoop ((posi, mkFlexibleIntFormatTypar g m) :: acc) (i+1, relLine, relCol+1) | ('l' | 'L') -> @@ -228,7 +231,7 @@ let parseFormatStringInternal (m:range) g (source: string option) fmt bty cty = failwithf "%s" <| FSComp.SR.forLIsUnnecessary() match fmt.[i] with | ('d' | 'i' | 'o' | 'u' | 'x' | 'X') -> - collectSpecifierLocation relLine relCol + collectSpecifierLocation relLine relCol 1 parseLoop ((posi, mkFlexibleIntFormatTypar g m) :: acc) (i+1, relLine, relCol+1) | _ -> failwithf "%s" <| FSComp.SR.forBadFormatSpecifier() @@ -236,38 +239,38 @@ let parseFormatStringInternal (m:range) g (source: string option) fmt bty cty = failwithf "%s" <| FSComp.SR.forHIsUnnecessary() | 'M' -> - collectSpecifierLocation relLine relCol + collectSpecifierLocation relLine relCol 1 parseLoop ((posi, g.decimal_ty) :: acc) (i+1, relLine, relCol+1) | ('f' | 'F' | 'e' | 'E' | 'g' | 'G') -> - collectSpecifierLocation relLine relCol + collectSpecifierLocation relLine relCol 1 parseLoop ((posi, mkFlexibleFloatFormatTypar g m) :: acc) (i+1, relLine, relCol+1) | 'b' -> checkOtherFlags ch - collectSpecifierLocation relLine relCol + collectSpecifierLocation relLine relCol 1 parseLoop ((posi, g.bool_ty) :: acc) (i+1, relLine, relCol+1) | 'c' -> checkOtherFlags ch - collectSpecifierLocation relLine relCol + collectSpecifierLocation relLine relCol 1 parseLoop ((posi, g.char_ty) :: acc) (i+1, relLine, relCol+1) | 's' -> checkOtherFlags ch - collectSpecifierLocation relLine relCol + collectSpecifierLocation relLine relCol 1 parseLoop ((posi, g.string_ty) :: acc) (i+1, relLine, relCol+1) | 'O' -> checkOtherFlags ch - collectSpecifierLocation relLine relCol + collectSpecifierLocation relLine relCol 1 parseLoop ((posi, NewInferenceType ()) :: acc) (i+1, relLine, relCol+1) | 'A' -> match info.numPrefixIfPos with | None // %A has BindingFlags=Public, %+A has BindingFlags=Public | NonPublic | Some '+' -> - collectSpecifierLocation relLine relCol + collectSpecifierLocation relLine relCol 1 parseLoop ((posi, NewInferenceType ()) :: acc) (i+1, relLine, relCol+1) | Some _ -> failwithf "%s" <| FSComp.SR.forDoesNotSupportPrefixFlag(ch.ToString(), (Option.get info.numPrefixIfPos).ToString()) @@ -275,12 +278,12 @@ let parseFormatStringInternal (m:range) g (source: string option) fmt bty cty = checkOtherFlags ch let xty = NewInferenceType () let fty = bty --> (xty --> cty) - collectSpecifierLocation relLine relCol + collectSpecifierLocation relLine relCol 2 parseLoop ((Option.map ((+)1) posi, xty) :: (posi, fty) :: acc) (i+1, relLine, relCol+1) | 't' -> checkOtherFlags ch - collectSpecifierLocation relLine relCol + collectSpecifierLocation relLine relCol 1 parseLoop ((posi, bty --> cty) :: acc) (i+1, relLine, relCol+1) | c -> failwithf "%s" <| FSComp.SR.forBadFormatSpecifierGeneral(String.make 1 c) diff --git a/src/fsharp/CheckFormatStrings.fsi b/src/fsharp/CheckFormatStrings.fsi index a3af9f5b32..4842aeb7f8 100755 --- a/src/fsharp/CheckFormatStrings.fsi +++ b/src/fsharp/CheckFormatStrings.fsi @@ -13,6 +13,6 @@ open Microsoft.FSharp.Compiler.Tast open Microsoft.FSharp.Compiler.TcGlobals open Microsoft.FSharp.Compiler.AbstractIL.Internal -val ParseFormatString : Range.range -> TcGlobals -> source: string option -> fmt: string -> bty: TType -> cty: TType -> dty: TType -> (TType * TType) * Range.range list +val ParseFormatString : Range.range -> TcGlobals -> source: string option -> fmt: string -> bty: TType -> cty: TType -> dty: TType -> (TType * TType) * (Range.range * int) list val TryCountFormatStringArguments : m:Range.range -> g:TcGlobals -> fmt:string -> bty:TType -> cty:TType -> int option diff --git a/src/fsharp/NameResolution.fs b/src/fsharp/NameResolution.fs index cba0e5e601..d6a59db9cf 100755 --- a/src/fsharp/NameResolution.fs +++ b/src/fsharp/NameResolution.fs @@ -1105,7 +1105,7 @@ type ITypecheckResultsSink = abstract NotifyEnvWithScope : range * NameResolutionEnv * AccessorDomain -> unit abstract NotifyExprHasType : pos * TType * Tastops.DisplayEnv * NameResolutionEnv * AccessorDomain * range -> unit abstract NotifyNameResolution : pos * Item * Item * ItemOccurence * Tastops.DisplayEnv * NameResolutionEnv * AccessorDomain * range -> unit - abstract NotifyFormatSpecifierLocation : range -> unit + abstract NotifyFormatSpecifierLocation : range * int -> unit abstract CurrentSource : string option let (|ValRefOfProp|_|) (pi : PropInfo) = pi.ArbitraryValRef @@ -1301,7 +1301,7 @@ type TcResolutions /// Represents container for all name resolutions that were met so far when typechecking some particular file -type TcSymbolUses(g, capturedNameResolutions : ResizeArray, formatSpecifierLocations: range[]) = +type TcSymbolUses(g, capturedNameResolutions : ResizeArray, formatSpecifierLocations: (range * int)[]) = member this.GetUsesOfSymbol(item) = [| for cnr in capturedNameResolutions do @@ -1312,7 +1312,7 @@ type TcSymbolUses(g, capturedNameResolutions : ResizeArray (Item * ItemOccurence * DisplayEnv * range)[] - member GetFormatSpecifierLocations : unit -> range[] + member GetFormatSpecifierLocationsAndArity : unit -> (range * int)[] /// An abstract type for reporting the results of name resolution and type checking @@ -241,7 +241,7 @@ type ITypecheckResultsSink = abstract NotifyEnvWithScope : range * NameResolutionEnv * AccessorDomain -> unit abstract NotifyExprHasType : pos * TType * DisplayEnv * NameResolutionEnv * AccessorDomain * range -> unit abstract NotifyNameResolution : pos * Item * Item * ItemOccurence * DisplayEnv * NameResolutionEnv * AccessorDomain * range -> unit - abstract NotifyFormatSpecifierLocation : range -> unit + abstract NotifyFormatSpecifierLocation : range * int -> unit abstract CurrentSource : string option type internal TcResultsSinkImpl = diff --git a/src/fsharp/TypeChecker.fs b/src/fsharp/TypeChecker.fs index d1432e8f3e..866565e9f2 100755 --- a/src/fsharp/TypeChecker.fs +++ b/src/fsharp/TypeChecker.fs @@ -1739,7 +1739,7 @@ let MakeAndPublishSimpleVals cenv env m names mergeNamesInOneNameresEnv = if not m.IsSynthetic then nameResolutions.Add(pos, a, b, occurence, denv, nenv, ad, m) member this.NotifyExprHasType(_, _, _, _, _, _) = assert false // no expr typings in MakeSimpleVals - member this.NotifyFormatSpecifierLocation _ = () + member this.NotifyFormatSpecifierLocation(_, _) = () member this.CurrentSource = None } use _h = WithNewTypecheckResultsSink(sink, cenv.tcSink) @@ -6314,8 +6314,8 @@ and TcConstStringExpr cenv overallTy env m tpenv s = match cenv.tcSink.CurrentSink with | None -> () | Some sink -> - for specifierLocation in specifierLocations do - sink.NotifyFormatSpecifierLocation specifierLocation + for specifierLocation,numArgs in specifierLocations do + sink.NotifyFormatSpecifierLocation(specifierLocation, numArgs) UnifyTypes cenv env m aty aty' UnifyTypes cenv env m ety ety' diff --git a/src/fsharp/vs/service.fs b/src/fsharp/vs/service.fs index 541fcf6669..d4c13c9ed2 100755 --- a/src/fsharp/vs/service.fs +++ b/src/fsharp/vs/service.fs @@ -1472,11 +1472,11 @@ type TypeCheckInfo [ for x in tcImports.GetImportedAssemblies() do yield FSharpAssembly(g, tcImports, x.FSharpViewOfMetadata) ] - // Not, this does not have to be a SyncOp, it can be called from any thread - member scope.GetFormatSpecifierLocations() = - sSymbolUses.GetFormatSpecifierLocations() + // Note, this does not have to be a SyncOp, it can be called from any thread + member scope.GetFormatSpecifierLocationsAndArity() = + sSymbolUses.GetFormatSpecifierLocationsAndArity() - // Not, this does not have to be a SyncOp, it can be called from any thread + // Note, this does not have to be a SyncOp, it can be called from any thread member scope.GetExtraColorizations() = [| for cnr in sResolutions.CapturedNameResolutions do match cnr with @@ -2053,13 +2053,15 @@ type FSharpCheckFileResults(errors: FSharpErrorInfo[], scopeOptX: TypeCheckInfo scope.GetSymbolUseAtLocation (line, lineStr, colAtEndOfNames, names) |> Option.map (fun (sym,_,_) -> sym)) - member info.GetFormatSpecifierLocations() = + info.GetFormatSpecifierLocationsAndArity() |> Array.map fst + + member info.GetFormatSpecifierLocationsAndArity() = threadSafeOp (fun () -> [| |]) (fun (scope, _builder, _reactor) -> - // This operation is not asynchronous - GetFormatSpecifierLocations can be run on the calling thread - scope.GetFormatSpecifierLocations()) + // This operation is not asynchronous - GetFormatSpecifierLocationsAndArity can be run on the calling thread + scope.GetFormatSpecifierLocationsAndArity()) member info.GetExtraColorizationsAlternate() = threadSafeOp diff --git a/src/fsharp/vs/service.fsi b/src/fsharp/vs/service.fsi index 668b1cc8f3..9663485ae5 100755 --- a/src/fsharp/vs/service.fsi +++ b/src/fsharp/vs/service.fsi @@ -260,8 +260,12 @@ type FSharpCheckFileResults = member GetExtraColorizationsAlternate : unit -> (range * FSharpTokenColorKind)[] /// Get the locations of format specifiers + [] member GetFormatSpecifierLocations : unit -> range[] + /// Get the locations of and number of arguments associated with format specifiers + member GetFormatSpecifierLocationsAndArity : unit -> (range*int)[] + /// Get all textual usages of all symbols throughout the file member GetAllUsesOfAllSymbolsInFile : unit -> Async diff --git a/tests/service/EditorTests.fs b/tests/service/EditorTests.fs index 431cc19f9d..a22519e220 100644 --- a/tests/service/EditorTests.fs +++ b/tests/service/EditorTests.fs @@ -385,30 +385,58 @@ let _ = List.map (sprintf @"%A let _ = (10, 12) ||> sprintf "%A %O" let _ = sprintf "\n%-8.1e+567" 1.0 -let _ = sprintf @"%O\n%-5s" "1" "2" """ +let _ = sprintf @"%O\n%-5s" "1" "2" +let _ = sprintf "%%" +let _ = sprintf " %*%" 2 +let _ = sprintf " %.*%" 2 +let _ = sprintf " %*.1%" 2 +let _ = sprintf " %*s" 10 "hello" +let _ = sprintf " %*.*%" 2 3 +let _ = sprintf " %*.*f" 2 3 4.5 +let _ = sprintf " %.*f" 3 4.5 +let _ = sprintf " %*.1f" 3 4.5 +let _ = sprintf " %6.*f" 3 4.5 +let _ = sprintf " %6.*%" 3 +let _ = printf " %a" (fun _ _ -> ()) 2 +let _ = printf " %*a" 3 (fun _ _ -> ()) 2 +""" let file = "/home/user/Test.fsx" let untyped, typeCheckResults = parseAndTypeCheckFileInProject(file, input) typeCheckResults.Errors |> shouldEqual [||] - typeCheckResults.GetFormatSpecifierLocations() - |> Array.map (fun range -> range.StartLine, range.StartColumn, range.EndLine, range.EndColumn) - |> shouldEqual [|(2, 45, 2, 46); - (3, 23, 3, 24); - (4, 38, 4, 39); - (5, 29, 5, 30); - (6, 17, 6, 19); - (7, 17, 7, 21); - (8, 17, 8, 22); - (9, 18, 9, 21); - (10, 18, 10, 20); - (12, 12, 12, 14); - (15, 12, 15, 14); - (16, 28, 16, 29); - (18, 30, 18, 31); - (19, 30, 19, 31); - (20, 19, 20, 24); - (21, 18, 21, 19); (21, 22, 21, 25)|] + typeCheckResults.GetFormatSpecifierLocationsAndArity() + |> Array.map (fun (range,numArgs) -> range.StartLine, range.StartColumn, range.EndLine, range.EndColumn, numArgs) + |> shouldEqual [|(2, 45, 2, 46, 1); + (3, 23, 3, 24, 1); + (4, 38, 4, 39, 1); + (5, 29, 5, 30, 1); + (6, 17, 6, 19, 2); + (7, 17, 7, 21, 1); + (8, 17, 8, 22, 1); + (9, 18, 9, 21, 1); + (10, 18, 10, 20, 1); + (12, 12, 12, 14, 1); + (15, 12, 15, 14, 1); + (16, 28, 16, 29, 1); + (18, 30, 18, 31, 1); + (19, 30, 19, 31, 1); + (20, 19, 20, 24, 1); + (21, 18, 21, 19, 1); + (21, 22, 21, 25, 1); + (22, 17, 22, 18, 0); + (23, 18, 23, 20, 1); + (24, 19, 24, 22, 1); + (25, 20, 25, 24, 1); + (26, 21, 26, 23, 2); + (27, 22, 27, 26, 2); + (28, 23, 28, 27, 3); + (29, 24, 29, 27, 2); + (30, 25, 30, 29, 2); + (31, 26, 31, 30, 2); + (32, 27, 32, 31, 1); + (33, 28, 33, 29, 2); + (34, 29, 34, 31, 3)|] [] let ``Printf specifiers for triple-quote strings`` () = @@ -426,12 +454,13 @@ let _ = List.iter(printfn \"\"\"%-A let untyped, typeCheckResults = parseAndTypeCheckFileInProject(file, input) typeCheckResults.Errors |> shouldEqual [||] - typeCheckResults.GetFormatSpecifierLocations() - |> Array.map (fun range -> range.StartLine, range.StartColumn, range.EndLine, range.EndColumn) - |> shouldEqual [|(2, 19, 2, 21); - (4, 12, 4, 14); - (6, 29, 6, 31); - (7, 29, 7, 30); (7, 33, 7, 34)|] + typeCheckResults.GetFormatSpecifierLocationsAndArity() + |> Array.map (fun (range, numArgs) -> range.StartLine, range.StartColumn, range.EndLine, range.EndColumn, numArgs) + |> shouldEqual [|(2, 19, 2, 21, 1); + (4, 12, 4, 14, 1); + (6, 29, 6, 31, 1); + (7, 29, 7, 30, 1); + (7, 33, 7, 34, 1)|] [] let ``Printf specifiers for user-defined functions`` () = @@ -446,25 +475,27 @@ let _ = debug "[LanguageService] Type checking fails for '%s' with content=%A an let untyped, typeCheckResults = parseAndTypeCheckFileInProject(file, input) typeCheckResults.Errors |> shouldEqual [||] - typeCheckResults.GetFormatSpecifierLocations() - |> Array.map (fun range -> range.StartLine, range.StartColumn, range.EndLine, range.EndColumn) - |> shouldEqual [|(3, 24, 3, 25); - (3, 29, 3, 30); - (4, 58, 4, 59); (4, 75, 4, 76); (4, 82, 4, 83); (4, 108, 4, 109)|] + typeCheckResults.GetFormatSpecifierLocationsAndArity() + |> Array.map (fun (range, numArgs) -> range.StartLine, range.StartColumn, range.EndLine, range.EndColumn, numArgs) + |> shouldEqual [|(3, 24, 3, 25, 1); + (3, 29, 3, 30, 1); + (4, 58, 4, 59, 1); + (4, 75, 4, 76, 1); + (4, 82, 4, 83, 1); + (4, 108, 4, 109, 1)|] [] let ``should not report format specifiers for illformed format strings`` () = let input = """ let _ = sprintf "%.7f %7.1A %7.f %--8.1f" -let _ = sprintf "%%A" let _ = sprintf "ABCDE" """ let file = "/home/user/Test.fsx" let untyped, typeCheckResults = parseAndTypeCheckFileInProject(file, input) - typeCheckResults.GetFormatSpecifierLocations() - |> Array.map (fun range -> range.StartLine, range.StartColumn, range.EndLine, range.EndColumn) + typeCheckResults.GetFormatSpecifierLocationsAndArity() + |> Array.map (fun (range, numArgs) -> range.StartLine, range.StartColumn, range.EndLine, range.EndColumn, numArgs) |> shouldEqual [||] []