Skip to content

Commit

Permalink
feat: Slice Out-of-Bound Check (#40)
Browse files Browse the repository at this point in the history
* basic bound check

* handling safe context (i.e., table test looping)
  • Loading branch information
notJoon authored Jul 31, 2024
1 parent c344189 commit 305fd15
Show file tree
Hide file tree
Showing 11 changed files with 423 additions and 2 deletions.
8 changes: 8 additions & 0 deletions formatter/fmt.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const (
SimplifySliceExpr = "simplify-slice-range"
CycloComplexity = "high-cyclomatic-complexity"
EmitFormat = "emit-format"
SliceBound = "slice-bounds-check"
)

// IssueFormatter is the interface that wraps the Format method.
Expand Down Expand Up @@ -51,6 +52,8 @@ func getFormatter(rule string) IssueFormatter {
return &CyclomaticComplexityFormatter{}
case EmitFormat:
return &EmitFormatFormatter{}
case SliceBound:
return &SliceBoundsCheckFormatter{}
default:
return &GeneralIssueFormatter{}
}
Expand All @@ -70,6 +73,11 @@ func buildSuggestion(result *strings.Builder, issue tt.Issue, lineStyle, suggest
result.WriteString(suggestionStyle.Sprintf("Suggestion:\n"))
for i, line := range strings.Split(issue.Suggestion, "\n") {
lineNum := fmt.Sprintf("%d", startLine+i)

if maxLineNumWidth < len(lineNum) {
maxLineNumWidth = len(lineNum)
}

result.WriteString(lineStyle.Sprintf("%s%s | ", padding[:maxLineNumWidth-len(lineNum)], lineNum))
result.WriteString(fmt.Sprintf("%s\n", line))
}
Expand Down
1 change: 1 addition & 0 deletions formatter/general.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const tabWidth = 8

var (
errorStyle = color.New(color.FgRed, color.Bold)
warningStyle = color.New(color.FgHiYellow, color.Bold)
ruleStyle = color.New(color.FgYellow, color.Bold)
fileStyle = color.New(color.FgCyan, color.Bold)
lineStyle = color.New(color.FgBlue, color.Bold)
Expand Down
43 changes: 43 additions & 0 deletions formatter/slice_bound.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package formatter

import (
"fmt"
"strings"

"github.com/gnoswap-labs/lint/internal"
tt "github.com/gnoswap-labs/lint/internal/types"
)

type SliceBoundsCheckFormatter struct{}

func (f *SliceBoundsCheckFormatter) Format(
issue tt.Issue,
snippet *internal.SourceCode,
) string {
var result strings.Builder

maxLineNumWidth := calculateMaxLineNumWidth(issue.End.Line)
padding := strings.Repeat(" ", maxLineNumWidth+1)

startLine := issue.Start.Line
endLine := issue.End.Line
for i := startLine; i <= endLine; i++ {
line := expandTabs(snippet.Lines[i-1])
result.WriteString(lineStyle.Sprintf("%s%d | ", padding[:maxLineNumWidth-len(fmt.Sprintf("%d", i))], i))
result.WriteString(line + "\n")
}

result.WriteString(lineStyle.Sprintf("%s| ", padding))
result.WriteString(messageStyle.Sprintf("%s\n", strings.Repeat("~", calculateMaxLineLength(snippet.Lines, startLine, endLine))))
result.WriteString(lineStyle.Sprintf("%s| ", padding))
result.WriteString(messageStyle.Sprintf("%s\n\n", issue.Message))

result.WriteString(warningStyle.Sprint("warning: "))
if issue.Category == "index-access" {
result.WriteString("Index access without bounds checking can lead to runtime panics.\n")
} else if issue.Category == "slice-expression" {
result.WriteString("Slice expressions without proper length checks may cause unexpected behavior.\n\n")
}

return result.String()
}
1 change: 1 addition & 0 deletions internal/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ func (e *Engine) registerDefaultRules() {
&SimplifySliceExprRule{},
&UnnecessaryConversionRule{},
&LoopAllocationRule{},
&SliceBoundCheckRule{},
&EmitFormatRule{},
&DetectCycleRule{},
&GnoSpecificRule{},
Expand Down
82 changes: 82 additions & 0 deletions internal/lints/lint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"go/ast"
"go/parser"
"go/token"
"os"
"path/filepath"
"runtime"
Expand Down Expand Up @@ -508,3 +509,84 @@ func TestFormatEmitCall(t *testing.T) {
})
}
}

func TestDetectSliceBoundCheck(t *testing.T) {
tests := []struct {
name string
code string
expected int
}{
{
name: "simple bound check",
code: `
package main
func main() {
arr := []int{1, 2, 3}
if i < len(arr) {
_ = arr[i]
}
}
`,
expected: 0,
},
{
name: "missing bound check",
code: `
package main
func main() {
arr := []int{1, 2, 3}
_ = arr[i]
}
`,
expected: 1,
},
{
name: "complex condition 2",
code: `
package main
type Item struct {
Name string
Value int
}
func main() {
sourceItems := []*Item{
{"item1", 10},
{"item2", 20},
{"item3", 30},
}
destinationItems := make([]*Item, 0, len(sourceItems))
i := 0
for _, item := range sourceItems {
destinationItems[i] = item
i++
}
}
`,
expected: 1,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fset := token.NewFileSet()
node, err := parser.ParseFile(fset, "", tt.code, 0)
if err != nil {
t.Fatalf("Failed to parse code: %v", err)
}

issues, err := DetectSliceBoundCheck("test.go", node, fset)
for i, issue := range issues {
t.Logf("Issue %d: %v", i, issue)
}
assert.NoError(t, err)
assert.Equal(
t, tt.expected, len(issues),
"Number of detected slice bound check issues doesn't match expected",
)
})
}
}
Loading

0 comments on commit 305fd15

Please sign in to comment.