Skip to content

Commit

Permalink
feat: convert to an Analyzer (#22)
Browse files Browse the repository at this point in the history
* feat: convert to an Analyzer

* chore: update GitHub Actions workflow

* refactor: move options
  • Loading branch information
ldez authored Dec 16, 2024
1 parent abcc656 commit 955cef7
Show file tree
Hide file tree
Showing 13 changed files with 281 additions and 268 deletions.
47 changes: 47 additions & 0 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: CI
on:
push:
branches:
- master
- main
pull_request:

env:
GO_VERSION: stable

jobs:
golangci-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
- name: lint
uses: golangci/golangci-lint-action@v6
with:
version: latest

tests:
needs: golangci-lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
- name: Run tests
run: go test -v -cover ./...

build:
needs: golangci-lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
- name: Build
run: go build -v ./cmd/funlen/
- name: Run
run: go run ./cmd/funlen/ ./...
38 changes: 0 additions & 38 deletions .github/workflows/test.yml

This file was deleted.

11 changes: 11 additions & 0 deletions cmd/funlen/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package main

import (
"golang.org/x/tools/go/analysis/singlechecker"

"github.com/ultraware/funlen"
)

func main() {
singlechecker.Main(funlen.NewAnalyzer(0, 0, false))
}
115 changes: 115 additions & 0 deletions funlen.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package funlen

import (
"go/ast"
"go/token"
"reflect"

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

const (
defaultLineLimit = 60
defaultStmtLimit = 40
)

func NewAnalyzer(lineLimit int, stmtLimit int, ignoreComments bool) *analysis.Analyzer {
if lineLimit == 0 {
lineLimit = defaultLineLimit
}

if stmtLimit == 0 {
stmtLimit = defaultStmtLimit
}

return &analysis.Analyzer{
Name: "funlen",
Doc: "Checks for long functions.",
URL: "https://github.com/ultraware/funlen",
Run: func(pass *analysis.Pass) (any, error) {
run(pass, lineLimit, stmtLimit, ignoreComments)
return nil, nil
},
}
}

func run(pass *analysis.Pass, lineLimit int, stmtLimit int, ignoreComments bool) {
for _, file := range pass.Files {
cmap := ast.NewCommentMap(pass.Fset, file, file.Comments)

for _, f := range file.Decls {
decl, ok := f.(*ast.FuncDecl)
if !ok || decl.Body == nil { // decl.Body can be nil for e.g. cgo
continue
}

if stmtLimit > 0 {
if stmts := parseStmts(decl.Body.List); stmts > stmtLimit {
pass.Reportf(decl.Name.Pos(), "Function '%s' has too many statements (%d > %d)", decl.Name.Name, stmts, stmtLimit)
continue
}
}

if lineLimit > 0 {
if lines := getLines(pass.Fset, decl, cmap.Filter(decl), ignoreComments); lines > lineLimit {
pass.Reportf(decl.Name.Pos(), "Function '%s' is too long (%d > %d)", decl.Name.Name, lines, lineLimit)
}
}
}
}
}

func getLines(fset *token.FileSet, f *ast.FuncDecl, cmap ast.CommentMap, ignoreComments bool) int {
lineCount := fset.Position(f.End()).Line - fset.Position(f.Pos()).Line - 1

if !ignoreComments {
return lineCount
}

var commentCount int

for _, c := range cmap.Comments() {
// If the CommentGroup's lines are inside the function
// count how many comments are in the CommentGroup
if (fset.Position(c.Pos()).Line > fset.Position(f.Pos()).Line) &&
(fset.Position(c.End()).Line < fset.Position(f.End()).Line) {
commentCount += len(c.List)
}
}

return lineCount - commentCount
}

func parseStmts(s []ast.Stmt) (total int) {
for _, v := range s {
total++
switch stmt := v.(type) {
case *ast.BlockStmt:
total += parseStmts(stmt.List) - 1
case *ast.ForStmt, *ast.RangeStmt, *ast.IfStmt,
*ast.SwitchStmt, *ast.TypeSwitchStmt, *ast.SelectStmt:
total += parseBodyListStmts(stmt)
case *ast.CaseClause:
total += parseStmts(stmt.Body)
case *ast.AssignStmt:
total += checkInlineFunc(stmt.Rhs[0])
case *ast.GoStmt:
total += checkInlineFunc(stmt.Call.Fun)
case *ast.DeferStmt:
total += checkInlineFunc(stmt.Call.Fun)
}
}
return
}

func checkInlineFunc(stmt ast.Expr) int {
if block, ok := stmt.(*ast.FuncLit); ok {
return parseStmts(block.Body.List)
}
return 0
}

func parseBodyListStmts(t any) int {
i := reflect.ValueOf(t).Elem().FieldByName(`Body`).Elem().FieldByName(`List`).Interface()
return parseStmts(i.([]ast.Stmt))
}
48 changes: 48 additions & 0 deletions funlen_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package funlen

import (
"testing"

"golang.org/x/tools/go/analysis/analysistest"
)

func TestAnalyzer(t *testing.T) {
testCases := []struct {
dir string
lineLimit int
stmtLimit int
ignoreComments bool
}{
{
dir: "too_many_statements",
lineLimit: 1,
stmtLimit: 1,
},
{
dir: "too_many_lines",
lineLimit: 1,
stmtLimit: 10,
},
{
dir: "too_many_statements_inline_func",
lineLimit: 1,
stmtLimit: 1,
},
{
dir: "ignores_comments",
lineLimit: 2,
stmtLimit: 2,
ignoreComments: true,
},
}

for _, test := range testCases {
t.Run(test.dir, func(t *testing.T) {
t.Parallel()

a := NewAnalyzer(test.lineLimit, test.stmtLimit, test.ignoreComments)

analysistest.Run(t, analysistest.TestData(), a, test.dir)
})
}
}
9 changes: 8 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
module github.com/ultraware/funlen

go 1.20
go 1.22.0

require golang.org/x/tools v0.28.0

require (
golang.org/x/mod v0.22.0 // indirect
golang.org/x/sync v0.10.0 // indirect
)
8 changes: 8 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8=
golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw=
Loading

0 comments on commit 955cef7

Please sign in to comment.