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

QF1013: detect strings that could be rewritten with a string literal #1580

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
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
2 changes: 2 additions & 0 deletions quickfix/analysis.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

102 changes: 102 additions & 0 deletions quickfix/qf1013/qf1013.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package qf1013

import (
"go/ast"
"go/token"
"strings"

"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/edit"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"

"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
)

const escapeCharacter = '\\'

var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "QF1013",
Run: run,
Requires: []*analysis.Analyzer{inspect.Analyzer},
},
Doc: &lint.RawDocumentation{
Title: `Simplify string by using raw string literal`,
Since: "2024.8",
Before: `"a string with an escape quote: \""`,
After: "`a string with an escape quote: \"`",
Severity: lint.SeverityHint,
},
})

func run(pass *analysis.Pass) (any, error) {
fn := func(node ast.Node) {
lit, ok := node.(*ast.BasicLit)
if !ok {
return // not a basic lit
}
if lit.Kind != token.STRING {
return // not a string
}

if strings.HasPrefix(lit.Value, "`") {
// already a raw string
return
}
val := lit.Value

// in string literals, any character may appear except back quote.
if strings.Contains(val, "`") {
// so, we cannot transform it to a raw string if a back quote appears
return
}

// this quickfix is intended to write simpler strings by using string literal
// but it is limited to the following use cases:
// - a quote is escaped
// - a backslash is escaped
// - everything else is ignored
if !strings.Contains(val, string(escapeCharacter)) {
// no blackslash in the string
// nothing to do
return
}

var cleanedVal string
var escapeCharacterFound bool
for _, c := range val[1 : len(val)-1] {
if !escapeCharacterFound {
escapeCharacterFound = (c == escapeCharacter)
if !escapeCharacterFound {
cleanedVal += string(c)
}
continue
}

// so the previous character was a backslash
// we reset the flag for next character in the string
escapeCharacterFound = false

switch c {
case escapeCharacter:
// we have an escaped backslash
case '"':
// we have an escaped quote
default:
// currently unsupported
return
}
cleanedVal += string(c)
}

msg := "Simplify string by using raw string literal"
fix := edit.Fix(msg, edit.ReplaceWithNode(pass.Fset, node, &ast.BasicLit{
Value: "`" + cleanedVal + "`",
}))
report.Report(pass, node, msg, report.Fixes(fix))
}
code.Preorder(pass, fn, (*ast.BasicLit)(nil))
return nil, nil
}
13 changes: 13 additions & 0 deletions quickfix/qf1013/qf1013_test.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package pkg

import "regexp"

func fn2(_ string) {
}

func fn() {
_ = "{\"foo\":true}" //@ diag(`Simplify string by using raw string literal`)
_ = "escaped backslash: \\ foo" //@ diag(`Simplify string by using raw string literal`)
_ = "double escaped new line: \\n" //@ diag(`Simplify string by using raw string literal`)
_ = "triple escaped new line: \\\\n" //@ diag(`Simplify string by using raw string literal`)

const _ = "escaped double quotes: \"" //@ diag(`Simplify string by using raw string literal`)
fn2("escaped double quotes: \"") //@ diag(`Simplify string by using raw string literal`)
a := "escaped double quotes: \"" //@ diag(`Simplify string by using raw string literal`)

fn2(a)
const _ = "abc"
_ = "abc"
_ = "escaped backslash \\ plus a back quote `"
_ = "escaped double quotes \" plus a back quote `"
_ = "escaped double quotes \" plus a new line \n"
_ = "escaped double quotes \" plus a tab \t"

_ = `abc`
_ = regexp.MustCompile("escaped backslash: \\ foo")
b := "escaped backslash: \\ foo"
_ = regexp.MustCompile(b)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package pkg

import "regexp"

func fn2(_ string) {
}

func fn() {
_ = `{"foo":true}` //@ diag(`Simplify string by using raw string literal`)
_ = `escaped backslash: \ foo` //@ diag(`Simplify string by using raw string literal`)
_ = `double escaped new line: \n` //@ diag(`Simplify string by using raw string literal`)
_ = `triple escaped new line: \\n` //@ diag(`Simplify string by using raw string literal`)

const _ = `escaped double quotes: "` //@ diag(`Simplify string by using raw string literal`)
fn2(`escaped double quotes: "`) //@ diag(`Simplify string by using raw string literal`)
a := `escaped double quotes: "` //@ diag(`Simplify string by using raw string literal`)

fn2(a)
const _ = "abc"
_ = "abc"
_ = "escaped backslash \\ plus a back quote `"
_ = "escaped double quotes \" plus a back quote `"
_ = "escaped double quotes \" plus a new line \n"
_ = "escaped double quotes \" plus a tab \t"

_ = `abc`
_ = regexp.MustCompile("escaped backslash: \\ foo")
b := "escaped backslash: \\ foo"
_ = regexp.MustCompile(b)
}
Loading