Skip to content

Commit

Permalink
FS3373 Quickfix: Replace with triple-quoted interpolated string (#364)
Browse files Browse the repository at this point in the history
  • Loading branch information
seclerp authored Apr 21, 2022
1 parent 595a5f1 commit 3804d34
Show file tree
Hide file tree
Showing 17 changed files with 342 additions and 171 deletions.
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
namespace rec JetBrains.ReSharper.Plugins.FSharp.Psi.Features.Daemon.Analyzers

open System
open System.Globalization
open JetBrains.ReSharper.Daemon.StringAnalysis
open JetBrains.ReSharper.Daemon.SyntaxHighlighting
open JetBrains.ReSharper.Feature.Services.Daemon
open JetBrains.ReSharper.Plugins.FSharp.Psi.Features
open JetBrains.ReSharper.Plugins.FSharp.Psi.Features.Daemon.Highlightings
open JetBrains.ReSharper.Plugins.FSharp.Psi.Impl.Tree
open JetBrains.ReSharper.Plugins.FSharp.Psi.Services.Util
open JetBrains.ReSharper.Plugins.FSharp.Psi.Tree
open JetBrains.ReSharper.Psi
open JetBrains.ReSharper.Psi.Parsing
Expand Down Expand Up @@ -81,175 +80,6 @@ type FSharpStringLexerBase(buffer) =
set value = base.Position <- value


type RegularStringLexer(buffer) =
inherit FSharpStringLexerBase(buffer)

static let maxUnicodeCodePoint = uint32 0x10FFFF

static let mutable isHexDigit = Func<_, _>(CharEx.IsHexDigitFast)
static let mutable isDigit = Func<_, _>(Char.IsDigit)

override x.StartOffset = 1
override x.EndOffset = 1

override x.AdvanceInternal() =
match x.Buffer[x.Position] with
| '\\' ->
x.Position <- x.Position + 1
if x.CanAdvance then x.ProcessEscapeSequence()
else StringTokenTypes.CHARACTER
| _ -> StringTokenTypes.CHARACTER

abstract ProcessEscapeSequence: unit -> TokenNodeType
default x.ProcessEscapeSequence() =
match x.Buffer[x.Position] with
| 'u' -> x.ProcessHexEscapeSequence(4)
| 'U' -> x.ProcessLongHexEscapeSequence()
| 'x' -> x.ProcessHexEscapeSequence(2)
| c when Char.IsDigit(c) -> x.ProcessNumericCharSequence()
| '"' | '\'' | '\\' | 'b' | 'n' | 'r' | 't' | 'a' | 'f' | 'v' -> StringTokenTypes.ESCAPE_CHARACTER
| _ -> StringTokenTypes.CHARACTER

member x.ProcessEscapeSequence(length, shift, matcher) =
let str = x.ProcessEscapeSequence(length, length, shift, matcher)
if str.Length = length then StringTokenTypes.ESCAPE_CHARACTER else StringTokenTypes.CHARACTER

member x.ProcessHexEscapeSequence(length) =
x.ProcessEscapeSequence(length, 1, isHexDigit)

member x.ProcessNumericCharSequence() =
x.ProcessEscapeSequence(3, 0, isDigit)

member x.ProcessLongHexEscapeSequence() =
let hex = x.ProcessEscapeSequence(8, max = 8, shift = 1, matcher = (fun c -> c.IsHexDigitFast()))
if hex.Length <> 8 then StringTokenTypes.CHARACTER else

let mutable codePoint = Unchecked.defaultof<uint32>
match UInt32.TryParse(hex, NumberStyles.HexNumber, null, &codePoint) with
| true when codePoint <= maxUnicodeCodePoint -> StringTokenTypes.ESCAPE_CHARACTER
| _ -> StringTokenTypes.CHARACTER

override x.ParseEscapeCharacter _ = raise (NotImplementedException())

type RegularInterpolatedStringLexer(buffer) =
inherit RegularStringLexer(buffer)

override x.StartOffset = 2

override x.AdvanceInternal() =
match InterpolatedStringLexer.advance x with
| null -> base.AdvanceInternal()
| nodeType -> nodeType

type RegularInterpolatedStringMiddleEndLexer(buffer) =
inherit RegularInterpolatedStringLexer(buffer)

override x.StartOffset = 1

type VerbatimStringLexer(buffer) =
inherit FSharpStringLexerBase(buffer)

override x.StartOffset = 2
override x.EndOffset = 1

override x.AdvanceInternal() =
if x.Buffer[x.Position] = '\"' then
x.Position <- x.Position + 1

if x.CanAdvance && x.Buffer[x.Position] = '\"' then StringTokenTypes.ESCAPE_CHARACTER else
StringTokenTypes.CHARACTER

else StringTokenTypes.CHARACTER

override x.ParseEscapeCharacter _ = raise (NotImplementedException())

type VerbatimByteArrayLexer(buffer) =
inherit VerbatimStringLexer(buffer)

override x.EndOffset = 2

type VerbatimInterpolatedStringLexer(buffer) =
inherit VerbatimStringLexer(buffer)

override x.StartOffset = 3

override x.AdvanceInternal() =
match InterpolatedStringLexer.advance x with
| null -> base.AdvanceInternal()
| nodeType -> nodeType

type VerbatimInterpolatedStringMiddleEndLexer(buffer) =
inherit VerbatimInterpolatedStringLexer(buffer)

override x.StartOffset = 1


type TripleQuoteStringLexer(buffer) =
inherit VerbatimStringLexer(buffer)

override x.StartOffset = 3
override x.EndOffset = 3

override x.AdvanceInternal() = StringTokenTypes.CHARACTER

type TripleQuoteInterpolatedStringLexer(buffer) =
inherit TripleQuoteStringLexer(buffer)

override x.StartOffset = 4

override x.AdvanceInternal() =
match InterpolatedStringLexer.advance x with
| null -> base.AdvanceInternal()
| nodeType -> nodeType

type TripleQuoteInterpolatedStringStartLexer(buffer) =
inherit TripleQuoteInterpolatedStringLexer(buffer)

override x.StartOffset = 4
override x.EndOffset = 1

type TripleQuoteInterpolatedStringMiddleLexer(buffer) =
inherit TripleQuoteInterpolatedStringLexer(buffer)

override x.StartOffset = 1
override x.EndOffset = 1

type TripleQuoteInterpolatedStringEndLexer(buffer) =
inherit TripleQuoteInterpolatedStringLexer(buffer)

override x.StartOffset = 1
override x.EndOffset = 3


type ByteArrayLexer(buffer) =
inherit RegularStringLexer(buffer)

override x.EndOffset = 2

override x.ProcessEscapeSequence() =
match x.Buffer[x.Position] with
| '\\' -> StringTokenTypes.ESCAPE_CHARACTER
| _ -> StringTokenTypes.CHARACTER

override x.ParseEscapeCharacter _ = raise (NotImplementedException())


module InterpolatedStringLexer =
let private checkChar (lexer: FSharpStringLexerBase) c =
lexer.Position <- lexer.Position + 1
if lexer.CanAdvance && lexer.Buffer[lexer.Position] = c then
StringTokenTypes.ESCAPE_CHARACTER
else
lexer.Position <- lexer.Position - 1
StringTokenTypes.INVALID_CHARACTER

let advance (lexer: FSharpStringLexerBase) =
match lexer.Buffer[lexer.Position] with
| '{' -> checkChar lexer '{'
| '}' -> checkChar lexer '}'
| _ -> null


[<ElementProblemAnalyzer(typeof<IInterpolatedStringExpr>)>]
type InterpolatedStringExprAnalyzer() =
inherit ElementProblemAnalyzer<IInterpolatedStringExpr>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ module FSharpErrors =
let [<Literal>] UnusedValue = 1182
let [<Literal>] UnusedThisVariable = 1183
let [<Literal>] CantTakeAddressOfExpression = 3236
let [<Literal>] SingleQuoteInSingleQuote = 3373

let [<Literal>] undefinedIndexerMessageSuffix = " does not define the field, constructor or member 'Item'."
let [<Literal>] undefinedGetSliceMessageSuffix = " does not define the field, constructor or member 'GetSlice'."
Expand Down Expand Up @@ -366,6 +367,9 @@ type FcsErrorsStageProcessBase(fsFile, daemonProcess) =
| CantTakeAddressOfExpression ->
createHighlightingFromNode CantTakeAddressOfExpressionError range

| SingleQuoteInSingleQuote ->
createHighlightingFromNodeWithMessage SingleQuoteInSingleQuoteError range error

| ObjectOfIndeterminateTypeUsedRequireTypeConstraint ->
createHighlightingFromNode IndexerIndeterminateTypeError range

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
<Compile Include="src\QuickFixes\ReplaceWithRegularStringFix.fs" />
<Compile Include="src\QuickFixes\RemoveIndexerDotFix.fs" />
<Compile Include="src\QuickFixes\FSharpImportTypeFix.fs" />
<Compile Include="src\QuickFixes\ReplaceWithTripleQuotedInterpolatedStringFix.fs" />
<ErrorsGen Include="..\FSharp.Psi.Services\src\Daemon\Highlightings\Errors.xml">
<Mode>QUICKFIX</Mode>
<Namespace>JetBrains.ReSharper.Plugins.FSharp.Psi.Features.Daemon.QuickFixes</Namespace>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
namespace JetBrains.ReSharper.Plugins.FSharp.Psi.Features.Daemon.QuickFixes

open JetBrains.ProjectModel
open JetBrains.ReSharper.Plugins.FSharp.Psi.Features.Daemon.Highlightings
open JetBrains.ReSharper.Plugins.FSharp.Psi.Features.Daemon.QuickFixes
open JetBrains.ReSharper.Plugins.FSharp.Psi.Parsing
open JetBrains.ReSharper.Plugins.FSharp.Psi.Services.Util
open JetBrains.ReSharper.Plugins.FSharp.Psi.Tree
open JetBrains.ReSharper.Psi.ExtensionsAPI.Tree
open JetBrains.ReSharper.Psi.Parsing
open JetBrains.ReSharper.Psi.Tree
open JetBrains.ReSharper.Plugins.FSharp.Psi.Features.StringLiteralsUtil
open JetBrains.ReSharper.Resources.Shell
open JetBrains.Text

type ReplaceWithTripleQuotedInterpolatedStringFix(error: SingleQuoteInSingleQuoteError) =
inherit FSharpQuickFixBase()

let createStart content =
FSharpTokenType.TRIPLE_QUOTE_INTERPOLATED_STRING_START.Create($"$\"\"\"{content}{{")

let createMiddle content =
FSharpTokenType.TRIPLE_QUOTE_INTERPOLATED_STRING_MIDDLE.Create($"}}{content}{{")

let createEnd content =
FSharpTokenType.TRIPLE_QUOTE_INTERPOLATED_STRING_END.Create($"}}{content}\"\"\"")

let checkRegularStringLiteral (literal: ITokenNode) =
let tokenContent = getStringContent (literal.GetTokenType()) (literal.GetText())
let lexer = RegularInterpolatedStringLexer(StringBuffer(tokenContent))

let mutable found = false
while not found && lexer.CanAdvance do
lexer.Advance()
found <- lexer.TokenType == StringTokenTypes.ESCAPE_CHARACTER
found

let processStringLiteral (textContentFactory: TokenNodeType -> string -> string) (literal: ITokenNode) =
let text = literal.GetText()
let tokenType = literal.GetTokenType()
let content = textContentFactory tokenType text

let resultingNode: ITreeNode =
match tokenType with
| tokenType when FSharpTokenType.InterpolatedStringsStart[tokenType] -> createStart content
| tokenType when FSharpTokenType.InterpolatedStringsMiddle[tokenType] -> createMiddle content
| tokenType when FSharpTokenType.InterpolatedStringsEnd[tokenType] -> createEnd content
| _ -> literal

if literal != resultingNode then
ModificationUtil.ReplaceChild(literal, resultingNode) |> ignore

let regularStringContentFactory (tokenType: TokenNodeType) (text: string) =
getStringContent tokenType text

let verbatimStringContentFactory (tokenType: TokenNodeType) (text: string) =
(getStringContent tokenType text).Replace("\"\"", "\"")

override this.IsAvailable _ =
if not <| isValid error.Expr then false else

let parentExpr = error.Expr.GetContainingNode<IInterpolatedStringExpr>()
if isNull parentExpr then false else

// Nested triple quoted interpolated strings represent not valid F# code, so ignore such possible case
let grandparentExpr = parentExpr.GetContainingNode<IInterpolatedStringExpr>()
if isNotNull grandparentExpr then false else

// Only regular interpolated strings without escape characters are supported
if parentExpr.FirstChild.GetTokenType() != FSharpTokenType.REGULAR_INTERPOLATED_STRING_START then true else
let containsEscapeCharacter = parentExpr.LiteralsEnumerable |> Seq.exists checkRegularStringLiteral
not containsEscapeCharacter

override this.Text = "Replace with triple-quoted interpolated string"

override this.ExecutePsiTransaction (_: ISolution) =
let interpolatedExpr = error.Expr.GetContainingNode<IInterpolatedStringExpr>()

use _ = WriteLockCookie.Create()

let firstChildType = interpolatedExpr.FirstChild.GetTokenType()
if firstChildType == FSharpTokenType.REGULAR_INTERPOLATED_STRING_START then
interpolatedExpr.Literals
|> Seq.iter (processStringLiteral regularStringContentFactory)
else if firstChildType == FSharpTokenType.VERBATIM_INTERPOLATED_STRING_START then
interpolatedExpr.Literals
|> Seq.iter (processStringLiteral verbatimStringContentFactory)
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
<Compile Include="src\Util\FSharpLambdaUtil.fs" />
<Compile Include="src\Util\FSharpExpectedTypesUtil.fs" />
<Compile Include="src\Util\EnumCaseLikeDeclarationUtil.fs" />
<Compile Include="src\Util\FSharpStringLexer.fs" />
<Compile Include="src\Daemon\Highlightings\FSharpErrorUtil.fs" />
<Compile Include="src\Daemon\Highlightings\ErrorHighlightings.fs" />
<ErrorsGen Include="src\Daemon\Highlightings\Errors.xml">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -486,4 +486,15 @@
<QuickFix>DeconstructPatternFix</QuickFix>
</Error>

<Error staticGroup="FSharpErrors" name="SingleQuoteInSingleQuote" ID="FS3373: Invalid interpolated string. Single quote or verbatim string literals may not be used in interpolated expressions in single quote or verbatim strings. Consider using an explicit 'let' binding for the interpolation expression or use a triple quote string as the outer string literal.">
<Parameter type="IFSharpExpression" name="expr"/>
<Parameter type="string" name="fcsMessage"/>
<Message value="{0}">
<Argument>fcsMessage</Argument>
</Message>
<Range>expr.GetHighlightingRange()</Range>
<Behavour overlapResolvePolicy="NONE"/>
<QuickFix>ReplaceWithTripleQuotedInterpolatedStringFix</QuickFix>
</Error>

</Errors>
Loading

0 comments on commit 3804d34

Please sign in to comment.