Skip to content

Commit

Permalink
feat: Deprecated Function Checker (#85)
Browse files Browse the repository at this point in the history
# Description

Add a deprecated functon checker that identifies the usage and suggests
a alternative functions if possible.

## Implementation Details

The core of this implementation is the `DeprecatedFuncChecker` struct
type, which maintains a map of deprecated functions along with their
alternative functions.

### How Deprecated Functions are Recognized

1. **Registration**: Deprecated functions are registered using thr
`Register` method, which takes three parameters
    - Package name
    - Function name
    - Alternative function (`packageName.funcName` format)
2. **Function Call Identification**: For each AST node, the checker
looks for `*ast.CallExpr` nodes, which represent function calls.
3. **Selector Expression Analysis**: It then checks if the function call
is a selector expression (`*ast.SelectorExpr`), which is typical for
`package.Function()` calls.
4. **Package and Function Matching**: The checker extracts the package
name and function name from the selector expression and compares them
against the registered deprecated functions.

## Usage Example

```go
checker := NewDeprecatedFuncChecker()
checker.Register("fmt", "Println", "fmt.Print")
checker.Register("os", "Remove", "os.RemoveAll")

deprecated, err := checker.Check(filename, astNode, fileSet)
if err != nil {
    // Handle error
}

for _, dep := range deprecated {
    fmt.Printf("Deprecated function %s.%s used. Consider using %s instead.\n",
        dep.Package, dep.Function, dep.Alternative)
}
```
  • Loading branch information
notJoon authored Oct 8, 2024
1 parent 9953cb4 commit acccdf1
Show file tree
Hide file tree
Showing 9 changed files with 517 additions and 32 deletions.
75 changes: 47 additions & 28 deletions formatter/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package formatter
import (
"fmt"
"strings"
"unicode"

"github.com/fatih/color"
"github.com/gnolang/tlin/internal"
Expand All @@ -19,6 +20,7 @@ const (
SliceBound = "slice-bounds-check"
Defers = "defer-issues"
MissingModPackage = "gno-mod-tidy"
DeprecatedFunc = "deprecated"
)

const tabWidth = 8
Expand Down Expand Up @@ -58,6 +60,8 @@ func GenerateFormattedIssue(issues []tt.Issue, snippet *internal.SourceCode) str
// If no specific formatter is found for the given rule, it returns a GeneralIssueFormatter.
func getFormatter(rule string) IssueFormatter {
switch rule {
case DeprecatedFunc:
return &DeprecatedFuncFormatter{}
case EarlyReturn:
return &EarlyReturnOpportunityFormatter{}
case SimplifySliceExpr:
Expand Down Expand Up @@ -146,7 +150,7 @@ func (b *IssueFormatterBuilder) AddCodeSnippet() *IssueFormatterBuilder {
continue
}

line := expandTabs(b.snippet.Lines[i-1])
line := b.snippet.Lines[i-1]
line = strings.TrimPrefix(line, commonIndent)
lineNum := fmt.Sprintf("%*d", maxLineNumWidth, i)

Expand Down Expand Up @@ -213,7 +217,8 @@ func (b *IssueFormatterBuilder) AddSuggestion() *IssueFormatterBuilder {
suggestionLines := strings.Split(b.issue.Suggestion, "\n")
for i, line := range suggestionLines {
lineNum := fmt.Sprintf("%*d", maxLineNumWidth, b.issue.Start.Line+i)
b.result.WriteString(lineStyle.Sprintf("%s | %s\n", lineNum, line))
b.result.WriteString(lineStyle.Sprintf("%s | ", lineNum))
b.result.WriteString(line + "\n")
}

b.result.WriteString(lineStyle.Sprintf("%s|\n", padding))
Expand Down Expand Up @@ -244,21 +249,6 @@ func calculateMaxLineNumWidth(endLine int) int {
return len(fmt.Sprintf("%d", endLine))
}

// expandTabs replaces tab characters('\t') with spaces.
// Assuming a table width of 8.
func expandTabs(line string) string {
var expanded strings.Builder
for i, ch := range line {
if ch == '\t' {
spaceCount := tabWidth - (i % tabWidth)
expanded.WriteString(strings.Repeat(" ", spaceCount))
} else {
expanded.WriteRune(ch)
}
}
return expanded.String()
}

// calculateVisualColumn calculates the visual column position
// in a string. taking into account tab characters.
func calculateVisualColumn(line string, column int) int {
Expand All @@ -279,26 +269,55 @@ func calculateVisualColumn(line string, column int) int {
return visualColumn
}

// findCommonIndent finds the common indent in the code snippet.
func findCommonIndent(lines []string) string {
if len(lines) == 0 {
return ""
}

commonIndentPrefix := strings.TrimLeft(lines[0], " \t")
commonIndentPrefix = lines[0][:len(lines[0])-len(commonIndentPrefix)]
// find first non-empty line's indent
var firstIndent []rune
for _, line := range lines {
// trimmed := strings.TrimSpace(line)
trimmed := strings.TrimLeftFunc(line, unicode.IsSpace)
if trimmed != "" {
firstIndent = []rune(line[:len(line)-len(trimmed)])
break
}
}

for _, line := range lines[1:] {
if strings.TrimSpace(line) == "" {
continue // ignore empty lines
if len(firstIndent) == 0 {
return ""
}

// search common indent for all non-empty lines
for _, line := range lines {
trimmed := strings.TrimLeftFunc(line, unicode.IsSpace)
if trimmed == "" {
continue
}

for !strings.HasPrefix(line, commonIndentPrefix) {
commonIndentPrefix = commonIndentPrefix[:len(commonIndentPrefix)-1]
if len(commonIndentPrefix) == 0 {
return ""
}
currentIndent := []rune(line[:len(line)-len(trimmed)])
firstIndent = commonPrefix(firstIndent, currentIndent)

if len(firstIndent) == 0 {
break
}
}

return commonIndentPrefix
return string(firstIndent)
}

// commonPrefix finds the common prefix of two strings.
func commonPrefix(a, b []rune) []rune {
minLen := len(a)
if len(b) < minLen {
minLen = len(b)
}
for i := 0; i < minLen; i++ {
if a[i] != b[i] {
return a[:i]
}
}
return a[:minLen]
}
18 changes: 18 additions & 0 deletions formatter/deprecated.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package formatter

import (
"github.com/gnolang/tlin/internal"
tt "github.com/gnolang/tlin/internal/types"
)

type DeprecatedFuncFormatter struct{}

func (f *DeprecatedFuncFormatter) Format(issue tt.Issue, snippet *internal.SourceCode) string {
builder := NewIssueFormatterBuilder(issue, snippet)
return builder.
AddHeader(errorHeader).
AddCodeSnippet().
AddUnderlineAndMessage().
AddNote().
Build()
}
8 changes: 4 additions & 4 deletions formatter/formatter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ func TestFormatIssuesWithArrows_UnnecessaryElse(t *testing.T) {
"package main",
"",
"func unnecessaryElse() bool {",
" if condition {",
" if condition {",
" return true",
" } else {",
" return false",
Expand Down Expand Up @@ -280,9 +280,9 @@ func TestFindCommonIndent(t *testing.T) {
{
name: "tab indent",
lines: []string{
"\tif foo {",
"\t\tprintln()",
"\t}",
" if foo {",
" println()",
" }",
},
expected: "\t",
},
Expand Down
121 changes: 121 additions & 0 deletions internal/checker/deprecate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package checker

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

// pkgPath -> funcName -> alternative
type deprecatedFuncMap map[string]map[string]string

// DeprecatedFunc represents a deprecated function.
type DeprecatedFunc struct {
Package string
Function string
Alternative string
Position token.Position
}

// DeprecatedFuncChecker checks for deprecated functions.
type DeprecatedFuncChecker struct {
deprecatedFuncs deprecatedFuncMap
}

func NewDeprecatedFuncChecker() *DeprecatedFuncChecker {
return &DeprecatedFuncChecker{
deprecatedFuncs: make(deprecatedFuncMap),
}
}

func (d *DeprecatedFuncChecker) Register(pkgName, funcName, alternative string) {
if _, ok := d.deprecatedFuncs[pkgName]; !ok {
d.deprecatedFuncs[pkgName] = make(map[string]string)
}
d.deprecatedFuncs[pkgName][funcName] = alternative
}

// Check checks a AST node for deprecated functions.
//
// TODO: use this in the linter rule implementation
func (d *DeprecatedFuncChecker) Check(
filename string,
node *ast.File,
fset *token.FileSet,
) ([]DeprecatedFunc, error) {
var found []DeprecatedFunc

packageAliases := make(map[string]string)
for _, imp := range node.Imports {
path, err := strconv.Unquote(imp.Path.Value)
if err != nil {
continue
}
name := ""
if imp.Name != nil {
name = imp.Name.Name
} else {
parts := strings.Split(path, "/")
name = parts[len(parts)-1]
}
packageAliases[name] = path
}

ast.Inspect(node, func(n ast.Node) bool {
call, ok := n.(*ast.CallExpr)
if !ok {
return true
}

switch fun := call.Fun.(type) {
case *ast.SelectorExpr:
ident, ok := fun.X.(*ast.Ident)
if !ok {
return true
}
pkgAlias := ident.Name
funcName := fun.Sel.Name

pkgPath, ok := packageAliases[pkgAlias]
if !ok {
// Not a package alias, possibly a method call
return true
}

if funcs, ok := d.deprecatedFuncs[pkgPath]; ok {
if alt, ok := funcs[funcName]; ok {
found = append(found, DeprecatedFunc{
Package: pkgPath,
Function: funcName,
Alternative: alt,
Position: fset.Position(call.Pos()),
})
}
}
case *ast.Ident:
// Handle functions imported via dot imports
funcName := fun.Name
// Check dot-imported packages
for alias, pkgPath := range packageAliases {
if alias != "." {
continue
}
if funcs, ok := d.deprecatedFuncs[pkgPath]; ok {
if alt, ok := funcs[funcName]; ok {
found = append(found, DeprecatedFunc{
Package: pkgPath,
Function: funcName,
Alternative: alt,
Position: fset.Position(call.Pos()),
})
break
}
}
}
}
return true
})

return found, nil
}
Loading

0 comments on commit acccdf1

Please sign in to comment.