Skip to content

Commit 56fbb87

Browse files
dsymeKevinRansom
authored andcommitted
Alignment with FCS to propagate specifier ranges in CheckFormatString.fs
1 parent 3431e61 commit 56fbb87

File tree

3 files changed

+126
-62
lines changed

3 files changed

+126
-62
lines changed

src/fsharp/CheckFormatStrings.fs

Lines changed: 124 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ open Microsoft.FSharp.Compiler.AbstractIL
88
open Microsoft.FSharp.Compiler.AbstractIL.Internal
99
open Microsoft.FSharp.Compiler.AbstractIL.Internal.Library
1010
open Microsoft.FSharp.Compiler.Ast
11+
open Microsoft.FSharp.Compiler.Range
1112
open Microsoft.FSharp.Compiler.ErrorLogger
1213
open Microsoft.FSharp.Compiler.Tast
1314
open Microsoft.FSharp.Compiler.Tastops
@@ -24,7 +25,7 @@ let lowestDefaultPriority = 0 (* See comment on TyparConstraint.DefaultsTo *)
2425

2526
let mkFlexibleFormatTypar m tys dflt =
2627
let tp = NewTypar (TyparKind.Type,TyparRigidity.Rigid,Typar(mkSynId m "fmt",HeadTypeStaticReq,true),false,TyparDynamicReq.Yes,[],false,false)
27-
tp.FixupConstraints [ TyparConstraint.SimpleChoice (tys,m); TyparConstraint.DefaultsTo (lowestDefaultPriority,dflt,m)];
28+
tp.FixupConstraints [ TyparConstraint.SimpleChoice (tys,m); TyparConstraint.DefaultsTo (lowestDefaultPriority,dflt,m)]
2829
copyAndFixupFormatTypar m tp
2930

3031
let mkFlexibleIntFormatTypar g m =
@@ -37,21 +38,46 @@ let mkFlexibleFloatFormatTypar g m =
3738
let isDigit c = ('0' <= c && c <= '9')
3839

3940
type FormatInfoRegister =
40-
{ mutable leftJustify : bool;
41-
mutable numPrefixIfPos : char option;
42-
mutable addZeros : bool;
41+
{ mutable leftJustify : bool
42+
mutable numPrefixIfPos : char option
43+
mutable addZeros : bool
4344
mutable precision : bool}
4445

4546
let newInfo ()=
46-
{ leftJustify = false;
47-
numPrefixIfPos = None;
48-
addZeros = false;
47+
{ leftJustify = false
48+
numPrefixIfPos = None
49+
addZeros = false
4950
precision = false}
5051

51-
let parseFormatStringInternal m g fmt bty cty =
52+
let parseFormatStringInternal (m:range) g (source: string option) fmt bty cty =
53+
// Offset is used to adjust ranges depending on whether input string is regular, verbatim or triple-quote.
54+
// We construct a new 'fmt' string since the current 'fmt' string doesn't distinguish between "\n" and escaped "\\n".
55+
let (offset, fmt) =
56+
match source with
57+
| Some source ->
58+
let source = source.Replace("\r\n", "\n").Replace("\r", "\n")
59+
let positions =
60+
source.Split('\n')
61+
|> Seq.map (fun s -> String.length s + 1)
62+
|> Seq.scan (+) 0
63+
|> Seq.toArray
64+
let length = source.Length
65+
if m.EndLine < positions.Length then
66+
let startIndex = positions.[m.StartLine-1] + m.StartColumn
67+
let endIndex = positions.[m.EndLine-1] + m.EndColumn - 1
68+
if startIndex < length-3 && source.[startIndex..startIndex+2] = "\"\"\"" then
69+
(3, source.[startIndex+3..endIndex-3])
70+
elif startIndex < length-2 && source.[startIndex..startIndex+1] = "@\"" then
71+
(2, source.[startIndex+2..endIndex-1])
72+
else (1, source.[startIndex+1..endIndex-1])
73+
else (1, fmt)
74+
| None -> (1, fmt)
75+
5276
let len = String.length fmt
5377

54-
let rec parseLoop acc i =
78+
let specifierLocations = ResizeArray()
79+
80+
let rec parseLoop acc (i, relLine, relCol) =
5581
if i >= len then
5682
let argtys =
5783
if acc |> List.forall (fun (p, _) -> p = None) then // without positional specifiers
@@ -60,11 +86,13 @@ let parseFormatStringInternal m g fmt bty cty =
6086
failwithf "%s" <| FSComp.SR.forPositionalSpecifiersNotPermitted()
6187
argtys
6288
elif System.Char.IsSurrogatePair(fmt,i) then
63-
parseLoop acc (i+2)
89+
parseLoop acc (i+2, relLine, relCol+2)
6490
else
6591
let c = fmt.[i]
6692
match c with
6793
| '%' ->
94+
let startCol = relCol
95+
let relCol = relCol+1
6896
let i = i+1
6997
if i >= len then failwithf "%s" <| FSComp.SR.forMissingFormatSpecifier()
7098
let info = newInfo()
@@ -73,58 +101,58 @@ let parseFormatStringInternal m g fmt bty cty =
73101
if i >= len then failwithf "%s" <| FSComp.SR.forMissingFormatSpecifier()
74102
match fmt.[i] with
75103
| '-' ->
76-
if info.leftJustify then failwithf "%s" <| FSComp.SR.forFlagSetTwice("-");
77-
info.leftJustify <- true;
104+
if info.leftJustify then failwithf "%s" <| FSComp.SR.forFlagSetTwice("-")
105+
info.leftJustify <- true
78106
flags(i+1)
79107
| '+' ->
80108
if info.numPrefixIfPos <> None then failwithf "%s" <| FSComp.SR.forPrefixFlagSpacePlusSetTwice()
81-
info.numPrefixIfPos <- Some '+';
109+
info.numPrefixIfPos <- Some '+'
82110
flags(i+1)
83111
| '0' ->
84-
if info.addZeros then failwithf "%s" <| FSComp.SR.forFlagSetTwice("0");
85-
info.addZeros <- true;
112+
if info.addZeros then failwithf "%s" <| FSComp.SR.forFlagSetTwice("0")
113+
info.addZeros <- true
86114
flags(i+1)
87115
| ' ' ->
88-
if info.numPrefixIfPos <> None then failwithf "%s" <| FSComp.SR.forPrefixFlagSpacePlusSetTwice();
89-
info.numPrefixIfPos <- Some ' ';
116+
if info.numPrefixIfPos <> None then failwithf "%s" <| FSComp.SR.forPrefixFlagSpacePlusSetTwice()
117+
info.numPrefixIfPos <- Some ' '
90118
flags(i+1)
91-
| '#' -> failwithf "%s" <| FSComp.SR.forHashSpecifierIsInvalid();
119+
| '#' -> failwithf "%s" <| FSComp.SR.forHashSpecifierIsInvalid()
92120
| _ -> i
93121

94122
let rec digitsPrecision i =
95-
if i >= len then failwithf "%s" <| FSComp.SR.forBadPrecision();
123+
if i >= len then failwithf "%s" <| FSComp.SR.forBadPrecision()
96124
match fmt.[i] with
97125
| c when isDigit c -> digitsPrecision (i+1)
98126
| _ -> i
99127

100128
let precision i =
101-
if i >= len then failwithf "%s" <| FSComp.SR.forBadWidth();
129+
if i >= len then failwithf "%s" <| FSComp.SR.forBadWidth()
102130
match fmt.[i] with
103131
| c when isDigit c -> info.precision <- true; false,digitsPrecision (i+1)
104132
| '*' -> info.precision <- true; true,(i+1)
105133
| _ -> failwithf "%s" <| FSComp.SR.forPrecisionMissingAfterDot()
106134

107135
let optionalDotAndPrecision i =
108-
if i >= len then failwithf "%s" <| FSComp.SR.forBadPrecision();
136+
if i >= len then failwithf "%s" <| FSComp.SR.forBadPrecision()
109137
match fmt.[i] with
110138
| '.' -> precision (i+1)
111139
| _ -> false,i
112140

113141
let rec digitsWidthAndPrecision i =
114-
if i >= len then failwithf "%s" <| FSComp.SR.forBadPrecision();
142+
if i >= len then failwithf "%s" <| FSComp.SR.forBadPrecision()
115143
match fmt.[i] with
116144
| c when isDigit c -> digitsWidthAndPrecision (i+1)
117145
| _ -> optionalDotAndPrecision i
118146

119147
let widthAndPrecision i =
120-
if i >= len then failwithf "%s" <| FSComp.SR.forBadPrecision();
148+
if i >= len then failwithf "%s" <| FSComp.SR.forBadPrecision()
121149
match fmt.[i] with
122150
| c when isDigit c -> false,digitsWidthAndPrecision i
123151
| '*' -> true,optionalDotAndPrecision (i+1)
124152
| _ -> false,optionalDotAndPrecision i
125153

126154
let rec digitsPosition n i =
127-
if i >= len then failwithf "%s" <| FSComp.SR.forBadPrecision();
155+
if i >= len then failwithf "%s" <| FSComp.SR.forBadPrecision()
128156
match fmt.[i] with
129157
| c when isDigit c -> digitsPosition (n*10 + int c - int '0') (i+1)
130158
| '$' -> Some n, i+1
@@ -136,14 +164,20 @@ let parseFormatStringInternal m g fmt bty cty =
136164
let p, i' = digitsPosition (int c - int '0') (i+1)
137165
if p = None then None, i else p, i'
138166
| _ -> None, i
139-
167+
168+
let oldI = i
140169
let posi, i = position i
170+
let relCol = relCol + i - oldI
141171

172+
let oldI = i
142173
let i = flags i
174+
let relCol = relCol + i - oldI
143175

176+
let oldI = i
144177
let widthArg,(precisionArg,i) = widthAndPrecision i
178+
let relCol = relCol + i - oldI
145179

146-
if i >= len then failwithf "%s" <| FSComp.SR.forBadPrecision();
180+
if i >= len then failwithf "%s" <| FSComp.SR.forBadPrecision()
147181

148182
let acc = if precisionArg then (Option.map ((+)1) posi, g.int_ty) :: acc else acc
149183

@@ -155,20 +189,36 @@ let parseFormatStringInternal m g fmt bty cty =
155189
failwithf "%s" <| FSComp.SR.forDoesNotSupportPrefixFlag(c.ToString(), (Option.get info.numPrefixIfPos).ToString())
156190

157191
let checkOtherFlags c =
158-
checkNoPrecision c;
159-
checkNoZeroFlag c;
192+
checkNoPrecision c
193+
checkNoZeroFlag c
160194
checkNoNumericPrefix c
161195

196+
let collectSpecifierLocation relLine relCol =
197+
match relLine with
198+
| 0 ->
199+
specifierLocations.Add(
200+
Range.mkFileIndexRange m.FileIndex
201+
(Range.mkPos m.StartLine (startCol + offset))
202+
(Range.mkPos m.StartLine (relCol + offset)))
203+
| _ ->
204+
specifierLocations.Add(
205+
Range.mkFileIndexRange m.FileIndex
206+
(Range.mkPos (m.StartLine + relLine) startCol)
207+
(Range.mkPos (m.StartLine + relLine) relCol))
208+
162209
let ch = fmt.[i]
163210
match ch with
164-
| '%' -> parseLoop acc (i+1)
211+
| '%' ->
212+
parseLoop acc (i+1, relLine, relCol+1)
165213

166214
| ('d' | 'i' | 'o' | 'u' | 'x' | 'X') ->
167-
if info.precision then failwithf "%s" <| FSComp.SR.forFormatDoesntSupportPrecision(ch.ToString());
168-
parseLoop ((posi, mkFlexibleIntFormatTypar g m) :: acc) (i+1)
215+
if info.precision then failwithf "%s" <| FSComp.SR.forFormatDoesntSupportPrecision(ch.ToString())
216+
collectSpecifierLocation relLine relCol
217+
parseLoop ((posi, mkFlexibleIntFormatTypar g m) :: acc) (i+1, relLine, relCol+1)
169218

170219
| ('l' | 'L') ->
171-
if info.precision then failwithf "%s" <| FSComp.SR.forFormatDoesntSupportPrecision(ch.ToString());
220+
if info.precision then failwithf "%s" <| FSComp.SR.forFormatDoesntSupportPrecision(ch.ToString())
221+
let relCol = relCol+1
172222
let i = i+1
173223

174224
// "bad format specifier ... In F# code you can use %d, %x, %o or %u instead ..."
@@ -178,65 +228,79 @@ let parseFormatStringInternal m g fmt bty cty =
178228
failwithf "%s" <| FSComp.SR.forLIsUnnecessary()
179229
match fmt.[i] with
180230
| ('d' | 'i' | 'o' | 'u' | 'x' | 'X') ->
181-
parseLoop ((posi, mkFlexibleIntFormatTypar g m) :: acc) (i+1)
231+
collectSpecifierLocation relLine relCol
232+
parseLoop ((posi, mkFlexibleIntFormatTypar g m) :: acc) (i+1, relLine, relCol+1)
182233
| _ -> failwithf "%s" <| FSComp.SR.forBadFormatSpecifier()
183234

184235
| ('h' | 'H') ->
185236
failwithf "%s" <| FSComp.SR.forHIsUnnecessary()
186237

187238
| 'M' ->
188-
parseLoop ((posi, g.decimal_ty) :: acc) (i+1)
239+
collectSpecifierLocation relLine relCol
240+
parseLoop ((posi, g.decimal_ty) :: acc) (i+1, relLine, relCol+1)
189241

190-
| ('f' | 'F' | 'e' | 'E' | 'g' | 'G') ->
191-
parseLoop ((posi, mkFlexibleFloatFormatTypar g m) :: acc) (i+1)
242+
| ('f' | 'F' | 'e' | 'E' | 'g' | 'G') ->
243+
collectSpecifierLocation relLine relCol
244+
parseLoop ((posi, mkFlexibleFloatFormatTypar g m) :: acc) (i+1, relLine, relCol+1)
192245

193246
| 'b' ->
194-
checkOtherFlags ch;
195-
parseLoop ((posi, g.bool_ty) :: acc) (i+1)
247+
checkOtherFlags ch
248+
collectSpecifierLocation relLine relCol
249+
parseLoop ((posi, g.bool_ty) :: acc) (i+1, relLine, relCol+1)
196250

197251
| 'c' ->
198-
checkOtherFlags ch;
199-
parseLoop ((posi, g.char_ty) :: acc) (i+1)
252+
checkOtherFlags ch
253+
collectSpecifierLocation relLine relCol
254+
parseLoop ((posi, g.char_ty) :: acc) (i+1, relLine, relCol+1)
200255

201256
| 's' ->
202-
checkOtherFlags ch;
203-
parseLoop ((posi, g.string_ty) :: acc) (i+1)
257+
checkOtherFlags ch
258+
collectSpecifierLocation relLine relCol
259+
parseLoop ((posi, g.string_ty) :: acc) (i+1, relLine, relCol+1)
204260

205261
| 'O' ->
206-
checkOtherFlags ch;
207-
parseLoop ((posi, NewInferenceType ()) :: acc) (i+1)
262+
checkOtherFlags ch
263+
collectSpecifierLocation relLine relCol
264+
parseLoop ((posi, NewInferenceType ()) :: acc) (i+1, relLine, relCol+1)
208265

209266
| 'A' ->
210267
match info.numPrefixIfPos with
211268
| None // %A has BindingFlags=Public, %+A has BindingFlags=Public | NonPublic
212-
| Some '+' -> parseLoop ((posi, NewInferenceType ()) :: acc) (i+1)
269+
| Some '+' ->
270+
collectSpecifierLocation relLine relCol
271+
parseLoop ((posi, NewInferenceType ()) :: acc) (i+1, relLine, relCol+1)
213272
| Some _ -> failwithf "%s" <| FSComp.SR.forDoesNotSupportPrefixFlag(ch.ToString(), (Option.get info.numPrefixIfPos).ToString())
214273

215274
| 'a' ->
216-
checkOtherFlags ch;
275+
checkOtherFlags ch
217276
let xty = NewInferenceType ()
218277
let fty = bty --> (xty --> cty)
219-
parseLoop ((Option.map ((+)1) posi, xty) :: (posi, fty) :: acc) (i+1)
278+
collectSpecifierLocation relLine relCol
279+
parseLoop ((Option.map ((+)1) posi, xty) :: (posi, fty) :: acc) (i+1, relLine, relCol+1)
220280

221281
| 't' ->
222-
checkOtherFlags ch;
223-
parseLoop ((posi, bty --> cty) :: acc) (i+1)
282+
checkOtherFlags ch
283+
collectSpecifierLocation relLine relCol
284+
parseLoop ((posi, bty --> cty) :: acc) (i+1, relLine, relCol+1)
224285

225286
| c -> failwithf "%s" <| FSComp.SR.forBadFormatSpecifierGeneral(String.make 1 c)
226-
227-
| _ -> parseLoop acc (i+1)
228-
parseLoop [] 0
229-
230-
let ParseFormatString m g fmt bty cty dty =
231-
let argtys = parseFormatStringInternal m g fmt bty cty
287+
288+
| '\n' -> parseLoop acc (i+1, relLine+1, 0)
289+
| _ -> parseLoop acc (i+1, relLine, relCol+1)
290+
291+
let results = parseLoop [] (0, 0, m.StartColumn)
292+
results, Seq.toList specifierLocations
293+
294+
let ParseFormatString m g source fmt bty cty dty =
295+
let argtys, specifierLocations = parseFormatStringInternal m g source fmt bty cty
232296
let aty = List.foldBack (-->) argtys dty
233297
let ety = mkTupledTy g argtys
234-
aty, ety
298+
(aty, ety), specifierLocations
235299

236300
let TryCountFormatStringArguments m g fmt bty cty =
237301
try
238-
parseFormatStringInternal m g fmt bty cty
239-
|> List.length
240-
|> Some
302+
let argtys, _specifierLocations = parseFormatStringInternal m g None fmt bty cty
303+
Some argtys.Length
241304
with _ ->
242-
None
305+
None
306+

src/fsharp/CheckFormatStrings.fsi

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,6 @@ open Microsoft.FSharp.Compiler.Tast
1313
open Microsoft.FSharp.Compiler.TcGlobals
1414
open Microsoft.FSharp.Compiler.AbstractIL.Internal
1515

16-
val ParseFormatString : Range.range -> TcGlobals -> string -> TType -> TType -> TType -> TType * TType
16+
val ParseFormatString : Range.range -> TcGlobals -> source: string option -> fmt: string -> bty: TType -> cty: TType -> dty: TType -> (TType * TType) * Range.range list
1717

1818
val TryCountFormatStringArguments : m:Range.range -> g:TcGlobals -> fmt:string -> bty:TType -> cty:TType -> int option

src/fsharp/TypeChecker.fs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6277,7 +6277,7 @@ and TcConstStringExpr cenv overallTy env m tpenv s =
62776277
let ty' = mkPrintfFormatTy cenv.g aty bty cty dty ety
62786278
if (not (isObjTy cenv.g overallTy) && AddCxTypeMustSubsumeTypeUndoIfFailed env.DisplayEnv cenv.css m overallTy ty') then
62796279
// Parse the format string to work out the phantom types
6280-
let aty',ety' = (try CheckFormatStrings.ParseFormatString m cenv.g s bty cty dty with Failure s -> error (Error(FSComp.SR.tcUnableToParseFormatString(s),m)))
6280+
let (aty',ety'),_specifierLocations = (try CheckFormatStrings.ParseFormatString m cenv.g None s bty cty dty with Failure s -> error (Error(FSComp.SR.tcUnableToParseFormatString(s),m)))
62816281
UnifyTypes cenv env m aty aty'
62826282
UnifyTypes cenv env m ety ety'
62836283
mkCallNewFormat cenv.g m aty bty cty dty ety (mkString cenv.g m s),tpenv

0 commit comments

Comments
 (0)