-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* add regex examples * detect repeated regex compilation * fix: Improve slice bounds check lint for range loop contexts (#45) * fix: Improve slice bounds check lint for range loop contexts * update docs
- Loading branch information
Showing
13 changed files
with
325 additions
and
0 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
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,95 @@ | ||
package lints | ||
|
||
import ( | ||
"os" | ||
"path/filepath" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func TestDetectRepeatedRegexCompilation(t *testing.T) { | ||
tests := []struct { | ||
name string | ||
code string | ||
expected int | ||
}{ | ||
{ | ||
name: "No repeated compilation", | ||
code: ` | ||
package main | ||
import "regexp" | ||
func noRepeat() { | ||
r1 := regexp.MustCompile("pattern1") | ||
r2 := regexp.MustCompile("pattern2") | ||
_ = r1 | ||
_ = r2 | ||
} | ||
`, | ||
expected: 0, | ||
}, | ||
{ | ||
name: "Repeated compilation", | ||
code: ` | ||
package main | ||
import "regexp" | ||
func withRepeat() { | ||
r1 := regexp.MustCompile("pattern") | ||
r2 := regexp.MustCompile("pattern") | ||
_ = r1 | ||
_ = r2 | ||
} | ||
`, | ||
expected: 1, | ||
}, | ||
{ | ||
name: "Multiple repeated compilations", | ||
code: ` | ||
package main | ||
import "regexp" | ||
func multipleRepeats() { | ||
r1 := regexp.MustCompile("pattern1") | ||
r2 := regexp.MustCompile("pattern1") | ||
r3 := regexp.MustCompile("pattern2") | ||
r4 := regexp.MustCompile("pattern2") | ||
_ = r1 | ||
_ = r2 | ||
_ = r3 | ||
_ = r4 | ||
} | ||
`, | ||
expected: 2, | ||
}, | ||
} | ||
|
||
for _, tt := range tests { | ||
t.Run(tt.name, func(t *testing.T) { | ||
tempDir, err := os.MkdirTemp("", "regex-test") | ||
require.NoError(t, err) | ||
defer os.RemoveAll(tempDir) | ||
|
||
tempFile := filepath.Join(tempDir, "test.go") | ||
err = os.WriteFile(tempFile, []byte(tt.code), 0o644) | ||
require.NoError(t, err) | ||
|
||
issues, err := DetectRepeatedRegexCompilation(tempFile) | ||
require.NoError(t, err) | ||
|
||
assert.Len(t, issues, tt.expected) | ||
|
||
if tt.expected > 0 { | ||
for _, issue := range issues { | ||
assert.Equal(t, "repeatedregexcompilation", issue.Rule) | ||
assert.Contains(t, issue.Message, "regexp.Compile called with same pattern more than once") | ||
} | ||
} | ||
}) | ||
} | ||
} |
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,120 @@ | ||
package lints | ||
|
||
import ( | ||
"go/ast" | ||
"go/token" | ||
|
||
tt "github.com/gnoswap-labs/lint/internal/types" | ||
"golang.org/x/tools/go/analysis" | ||
"golang.org/x/tools/go/packages" | ||
) | ||
|
||
var RepeatedRegexCompilationAnalyzer = &analysis.Analyzer{ | ||
Name: "repeatedregexcompilation", | ||
Doc: "Checks for repeated compilation of the same regex pattern", | ||
Run: runRepeatedRegexCompilation, | ||
} | ||
|
||
func DetectRepeatedRegexCompilation(filename string) ([]tt.Issue, error) { | ||
issues, err := runAnalyzer(filename, RepeatedRegexCompilationAnalyzer) | ||
if err != nil { | ||
return nil, err | ||
} | ||
return issues, nil | ||
} | ||
|
||
func runAnalyzer(filename string, a *analysis.Analyzer) ([]tt.Issue, error) { | ||
cfg := &packages.Config{ | ||
Mode: packages.NeedFiles | packages.NeedSyntax | packages.NeedTypesInfo | packages.NeedTypes, | ||
Tests: false, | ||
} | ||
|
||
pkgs, err := packages.Load(cfg, filename) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
var diagnostics []analysis.Diagnostic | ||
pass := &analysis.Pass{ | ||
Analyzer: a, | ||
Fset: pkgs[0].Fset, | ||
Files: pkgs[0].Syntax, | ||
Pkg: pkgs[0].Types, | ||
TypesInfo: pkgs[0].TypesInfo, | ||
ResultOf: make(map[*analysis.Analyzer]interface{}), | ||
Report: func(d analysis.Diagnostic) { | ||
diagnostics = append(diagnostics, d) | ||
}, | ||
} | ||
|
||
_, err = a.Run(pass) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
var issues []tt.Issue | ||
for _, diag := range diagnostics { | ||
issues = append(issues, tt.Issue{ | ||
Rule: a.Name, | ||
Filename: filename, | ||
Start: pass.Fset.Position(diag.Pos), | ||
End: pass.Fset.Position(diag.End), | ||
Message: diag.Message, | ||
}) | ||
} | ||
|
||
return issues, nil | ||
} | ||
|
||
func runRepeatedRegexCompilation(pass *analysis.Pass) (interface{}, error) { | ||
for _, file := range pass.Files { | ||
ast.Inspect(file, func(n ast.Node) bool { | ||
funcDecl, ok := n.(*ast.FuncDecl) | ||
if !ok { | ||
return true | ||
} | ||
|
||
regexPatterns := make(map[string]token.Pos) | ||
ast.Inspect(funcDecl.Body, func(node ast.Node) bool { | ||
callExpr, ok := node.(*ast.CallExpr) | ||
if !ok { | ||
return true | ||
} | ||
|
||
if isRegexpCompile(callExpr) { | ||
if pattern, ok := getRegexPattern(callExpr); ok { | ||
if firstPos, exists := regexPatterns[pattern]; exists { | ||
pass.Reportf(callExpr.Pos(), "regexp.Compile called with same pattern more than once. First occurrence at line %d", pass.Fset.Position(firstPos).Line) | ||
} else { | ||
regexPatterns[pattern] = callExpr.Pos() | ||
} | ||
} | ||
} | ||
|
||
return true | ||
}) | ||
|
||
return true | ||
}) | ||
} | ||
|
||
return nil, nil | ||
} | ||
|
||
func isRegexpCompile(callExpr *ast.CallExpr) bool { | ||
if selExpr, ok := callExpr.Fun.(*ast.SelectorExpr); ok { | ||
if ident, ok := selExpr.X.(*ast.Ident); ok { | ||
return ident.Name == "regexp" && (selExpr.Sel.Name == "Compile" || selExpr.Sel.Name == "MustCompile") | ||
} | ||
} | ||
return false | ||
} | ||
|
||
func getRegexPattern(callExpr *ast.CallExpr) (string, bool) { | ||
if len(callExpr.Args) > 0 { | ||
if lit, ok := callExpr.Args[0].(*ast.BasicLit); ok && lit.Kind == token.STRING { | ||
return lit.Value, true | ||
} | ||
} | ||
return "", false | ||
} |
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,15 @@ | ||
package main | ||
|
||
import ( | ||
"regexp" | ||
|
||
"gno.land/p/demo/ufmt" | ||
) | ||
|
||
func repeatedRegexCompilation(inputs []string) { | ||
for _, input := range inputs { | ||
regex := regexp.MustCompile(`\d+`) | ||
matches := regex.FindAllString(input, -1) | ||
ufmt.Println(matches) | ||
} | ||
} |
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,16 @@ | ||
package main | ||
|
||
import ( | ||
"regexp" | ||
|
||
"gno.land/p/demo/ufmt" | ||
) | ||
|
||
|
||
func avoidRepeatedRegexCompilation(inputs []string) { | ||
regex := regexp.MustCompile(`\d+`) | ||
for _, input := range inputs { | ||
matches := regex.FindAllString(input, -1) | ||
ufmt.Println(matches) | ||
} | ||
} |
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,9 @@ | ||
package main | ||
|
||
import ( | ||
"regexp" | ||
) | ||
|
||
func complexRegexExample() { | ||
regex := regexp.MustCompile(`^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$`) | ||
} |
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,10 @@ | ||
package main | ||
|
||
import "regexp" | ||
|
||
func unnecessaryCaptureGroupExample() { | ||
regex := regexp.MustCompile(`(https?://\S+)`) | ||
} | ||
|
||
// minimized: | ||
// (?:https?://\S+)` |
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,11 @@ | ||
package main | ||
|
||
import "regexp" | ||
|
||
func greedyQuantifierExample(largeInput string) { | ||
regex := regexp.MustCompile(`<.*>`) | ||
matches := regex.FindAllString(largeInput, -1) | ||
} | ||
|
||
// minimized: | ||
// <.*?> |
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,17 @@ | ||
package main | ||
|
||
import ( | ||
"regexp" | ||
) | ||
|
||
func preferStringMethodExample(input string) bool { | ||
return regexp.MustCompile(`^[a-zA-Z]+$`).MatchString(input) | ||
} | ||
|
||
// Suggestion: | ||
// for _, char := range input { | ||
// if !unicode.IsLetter(char) { | ||
// return false | ||
// } | ||
// } | ||
// return true |
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,10 @@ | ||
package main | ||
|
||
import "regexp" | ||
|
||
func withRepeat() { | ||
r1 := regexp.MustCompile("pattern") | ||
r2 := regexp.MustCompile("pattern") | ||
_ = r1 | ||
_ = r2 | ||
} |