-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
new checker 'encoded-compare' (#179)
* merge master * return README formatting * self-review fixes * self-review fixes * self-review fixes * encoded-compare: more cases from real world * self-review fixes
- Loading branch information
Showing
18 changed files
with
1,140 additions
and
54 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
198 changes: 198 additions & 0 deletions
198
analyzer/testdata/src/checkers-default/encoded-compare/encoded_compare_test.go
Large diffs are not rendered by default.
Oops, something went wrong.
198 changes: 198 additions & 0 deletions
198
analyzer/testdata/src/checkers-default/encoded-compare/encoded_compare_test.go.golden
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
package analysisutil | ||
|
||
import "strings" | ||
|
||
var whitespaceRemover = strings.NewReplacer("\n", "", "\\n", "", "\t", "", "\\t", "", " ", "") | ||
|
||
// IsJSONLike returns true if the string has JSON format features. | ||
// A positive result can be returned for invalid JSON as well. | ||
func IsJSONLike(s string) bool { | ||
s = whitespaceRemover.Replace(unescape(s)) | ||
|
||
var startMatch bool | ||
for _, prefix := range []string{ | ||
`{{`, `{[`, `{"`, | ||
`[{{`, `[{[`, `[{"`, | ||
} { | ||
if strings.HasPrefix(s, prefix) { | ||
startMatch = true | ||
break | ||
} | ||
} | ||
if !startMatch { | ||
return false | ||
} | ||
|
||
for _, keyValue := range []string{`":{`, `":[`, `":"`} { | ||
if strings.Contains(s, keyValue) { | ||
return true | ||
} | ||
} | ||
return false | ||
|
||
// NOTE(a.telyshev): We do not check the end of the string, because this is usually a field for typos. | ||
// And one of the reasons for using JSON-specific assertions is to catch typos like this. | ||
} | ||
|
||
func unescape(s string) string { | ||
s = strings.ReplaceAll(s, `\"`, `"`) | ||
s = unquote(s, `"`) | ||
s = unquote(s, "`") | ||
return s | ||
} | ||
|
||
func unquote(s string, q string) string { | ||
return strings.TrimLeft(strings.TrimRight(s, q), q) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
package analysisutil_test | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/Antonboom/testifylint/internal/analysisutil" | ||
) | ||
|
||
func TestIsJSONLike(t *testing.T) { | ||
cases := []struct { | ||
in string | ||
expected bool | ||
}{ | ||
{ | ||
in: `[{"name": "values-files", "array": ["values-dev.yaml"]}, {"name": "helm-parameters", "map": {"image.tag": "v1.2.3"}}]`, | ||
expected: true, | ||
}, | ||
{ | ||
in: `{"labels":{"aaa":"111"},"annotations":{"ccc":"333"}}`, | ||
expected: true, | ||
}, | ||
{ | ||
in: "{\"message\":\"No user was found in the LDAP server(s) with that username\"}", | ||
expected: true, | ||
}, | ||
{ | ||
in: `"{\n \"first\": \"Tobi\",\n \"last\": \"Ferret\"\n}"`, | ||
expected: true, | ||
}, | ||
{ | ||
in: `"{\"message\":\"No user was found in the LDAP server(s) with that username\"}"`, | ||
expected: true, | ||
}, | ||
{ | ||
in: `{"uuid": "b65b1a22-db6d-4f5a-9b3d-7302368a82e6"}`, | ||
expected: true, | ||
}, | ||
{ | ||
in: `apiVersion: 3`, | ||
expected: false, | ||
}, | ||
{ | ||
in: `[{}]`, | ||
expected: false, | ||
}, | ||
{ | ||
in: `{{ .TemplateVar }}`, | ||
expected: false, | ||
}, | ||
{ | ||
in: `{{-.TemplateVar}}`, | ||
expected: false, | ||
}, | ||
} | ||
|
||
for _, tt := range cases { | ||
t.Run("", func(t *testing.T) { | ||
isJSON := analysisutil.IsJSONLike(tt.in) | ||
if isJSON != tt.expected { | ||
t.FailNow() | ||
} | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
package checkers | ||
|
||
import ( | ||
"go/ast" | ||
|
||
"golang.org/x/tools/go/analysis" | ||
) | ||
|
||
// EncodedCompare detects situations like | ||
// | ||
// assert.Equal(t, `{"foo": "bar"}`, body) | ||
// assert.EqualValues(t, `{"foo": "bar"}`, body) | ||
// assert.Exactly(t, `{"foo": "bar"}`, body) | ||
// assert.Equal(t, expectedJSON, resultJSON) | ||
// assert.Equal(t, expBodyConst, w.Body.String()) | ||
// assert.Equal(t, fmt.Sprintf(`{"value":"%s"}`, hexString), result) | ||
// assert.Equal(t, "{}", json.RawMessage(resp)) | ||
// assert.Equal(t, expJSON, strings.Trim(string(resultJSONBytes), "\n")) // + Replace, ReplaceAll, TrimSpace | ||
// | ||
// assert.Equal(t, expectedYML, conf) | ||
// | ||
// and requires | ||
// | ||
// assert.JSONEq(t, `{"foo": "bar"}`, body) | ||
// assert.YAMLEq(t, expectedYML, conf) | ||
type EncodedCompare struct{} | ||
|
||
// NewEncodedCompare constructs EncodedCompare checker. | ||
func NewEncodedCompare() EncodedCompare { return EncodedCompare{} } | ||
func (EncodedCompare) Name() string { return "encoded-compare" } | ||
|
||
func (checker EncodedCompare) Check(pass *analysis.Pass, call *CallMeta) *analysis.Diagnostic { | ||
switch call.Fn.NameFTrimmed { | ||
case "Equal", "EqualValues", "Exactly": | ||
default: | ||
return nil | ||
} | ||
|
||
if len(call.Args) < 2 { | ||
return nil | ||
} | ||
lhs, rhs := call.Args[0], call.Args[1] | ||
|
||
a, aIsExplicitJSON := checker.unwrap(pass, call.Args[0]) | ||
b, bIsExplicitJSON := checker.unwrap(pass, call.Args[1]) | ||
|
||
var proposed string | ||
switch { | ||
case aIsExplicitJSON, bIsExplicitJSON, isJSONStyleExpr(pass, a), isJSONStyleExpr(pass, b): | ||
proposed = "JSONEq" | ||
case isYAMLStyleExpr(a), isYAMLStyleExpr(b): | ||
proposed = "YAMLEq" | ||
} | ||
|
||
if proposed != "" { | ||
return newUseFunctionDiagnostic(checker.Name(), call, proposed, | ||
newSuggestedFuncReplacement(call, proposed, analysis.TextEdit{ | ||
Pos: lhs.Pos(), | ||
End: lhs.End(), | ||
NewText: formatWithStringCastForBytes(pass, a), | ||
}, analysis.TextEdit{ | ||
Pos: rhs.Pos(), | ||
End: rhs.End(), | ||
NewText: formatWithStringCastForBytes(pass, b), | ||
})) | ||
} | ||
return nil | ||
} | ||
|
||
// unwrap unwraps expression from string, []byte, strings.Replace(All), strings.Trim(Space) and json.RawMessage conversions. | ||
// Returns true in the second argument, if json.RawMessage was in the chain. | ||
func (checker EncodedCompare) unwrap(pass *analysis.Pass, e ast.Expr) (ast.Expr, bool) { | ||
ce, ok := e.(*ast.CallExpr) | ||
if !ok { | ||
return e, false | ||
} | ||
if len(ce.Args) == 0 { | ||
return e, false | ||
} | ||
|
||
if isJSONRawMessageCast(pass, ce) { | ||
if isNil(ce.Args[0]) { // NOTE(a.telyshev): Ignore json.RawMessage(nil) case. | ||
return checker.unwrap(pass, ce.Args[0]) | ||
} | ||
|
||
v, _ := checker.unwrap(pass, ce.Args[0]) | ||
return v, true | ||
} | ||
|
||
if isIdentWithName("string", ce.Fun) || | ||
isByteArray(ce.Fun) || | ||
isStringsReplaceCall(pass, ce) || | ||
isStringsReplaceAllCall(pass, ce) || | ||
isStringsTrimCall(pass, ce) || | ||
isStringsTrimSpaceCall(pass, ce) { | ||
return checker.unwrap(pass, ce.Args[0]) | ||
} | ||
return e, false | ||
} |
Oops, something went wrong.