Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 38 additions & 14 deletions analysis/analyzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"os"
"path/filepath"
"reflect"
"slices"

sitter "github.com/smacker/go-tree-sitter"
Expand Down Expand Up @@ -50,14 +51,19 @@ type Analyzer struct {
Category Category
Severity Severity
Language Language
Run func(*Pass) (interface{}, error)
Requires []*Analyzer
Run func(*Pass) (any, error)
ResultType reflect.Type
}

type Pass struct {
Analyzer *Analyzer
FileContext *ParseResult
Files []*ParseResult
ResultOf map[*Analyzer]any
Report func(*Pass, *sitter.Node, string)
// TODO (opt): the cache should ideally not be stored in-memory
ResultCache map[*Analyzer]map[*ParseResult]any
}

func walkTree(node *sitter.Node, f func(*sitter.Node)) {
Expand Down Expand Up @@ -88,11 +94,21 @@ var defaultIgnoreDirs = []string{
".vitepress",
}

func findAnalyzers(analyzer *Analyzer) []*Analyzer {
analyzers := []*Analyzer{}
for _, req := range analyzer.Requires {
analyzers = append(analyzers, findAnalyzers(req)...)
}
analyzers = append(analyzers, analyzer)
return analyzers
}

func RunAnalyzers(path string, analyzers []*Analyzer, fileFilter func(string) bool) ([]*Issue, error) {
raisedIssues := []*Issue{}
langAnalyzerMap := make(map[Language][]*Analyzer)

for _, analyzer := range analyzers {
langAnalyzerMap[analyzer.Language] = append(langAnalyzerMap[analyzer.Language], analyzer)
langAnalyzerMap[analyzer.Language] = append(langAnalyzerMap[analyzer.Language], findAnalyzers(analyzer)...)
}

trees := make(map[Language][]*ParseResult)
Expand Down Expand Up @@ -138,24 +154,32 @@ func RunAnalyzers(path string, analyzers []*Analyzer, fileFilter func(string) bo
}

for lang, analyzers := range langAnalyzerMap {
for _, analyzer := range analyzers {
allFiles := trees[lang]
if len(allFiles) == 0 {
continue
}
pass := &Pass{
Files: trees[lang],
Report: reportFunc,
ResultOf: make(map[*Analyzer]any),
ResultCache: make(map[*Analyzer]map[*ParseResult]any),
}

for _, file := range pass.Files {
pass.FileContext = file
for _, analyzer := range analyzers {
pass.Analyzer = analyzer

for _, file := range allFiles {
pass := &Pass{
Analyzer: analyzer,
FileContext: file,
Files: trees[lang],
Report: reportFunc,
if len(pass.Files) == 0 {
continue
}

_, err := analyzer.Run(pass)
result, err := analyzer.Run(pass)
if err != nil {
return raisedIssues, err
}

pass.ResultOf[analyzer] = result
if _, ok := pass.ResultCache[analyzer]; !ok {
pass.ResultCache[analyzer] = make(map[*ParseResult]any)
}
pass.ResultCache[analyzer][file] = result
}
}
}
Expand Down
1 change: 1 addition & 0 deletions analysis/scope_ts.go → analysis/ts_scope.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ var ScopeNodes = []string{
"for_statement",
"for_in_statement",
"for_of_statement",
"program",
}

func (ts *TsScopeBuilder) NodeCreatesScope(node *sitter.Node) bool {
Expand Down
File renamed without changes.
4 changes: 2 additions & 2 deletions checkers/go/cgi_import.test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package main
package golang

import (
// <expect-error> usage of cgi package
"net/http/cgi"
"fmt"
"net/http"
"net/http/cgi"
)

func cgi_test() {
Expand Down
25 changes: 9 additions & 16 deletions checkers/go/fmt_print_in_prod.test.go
Original file line number Diff line number Diff line change
@@ -1,31 +1,24 @@
// <expect-error>
fmt.Print()

// <expect-error>
fmt.Println()


package main
package golang

import (
"fmt"
"logger"
"log"
)

func main() {
func testFmtInProd() {
// <expect-error>
fmt.Print("Hello, ")
fmt.Print("Hello, ")
// <expect-error>
fmt.Print("World!")
fmt.Print("World!")
// <expect-error>
fmt.Println("Hello, World!")
fmt.Println("Hello, World!")
// <expect-error>
fmt.Println("Line 1")
fmt.Println("Line 1")
// <expect-error>
fmt.Printf("Name: %s, Age: %d\n", name, age)

// Safe
logger.Info("Hello, World!")
log.Println("Hello, World!")
//Safe
logger.Debug("Hello, World!")
log.Println("Hello, World!")
}
2 changes: 1 addition & 1 deletion checkers/go/http_file_server.test.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package main
package golang

import (
"log"
Expand Down
6 changes: 3 additions & 3 deletions checkers/go/jwt_none_algorithm.test.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package main
package golang

import (
"fmt"
"os"
"time"

"github.com/golang-jwt/jwt/v5"
"jwt"
)

// CustomClaims defines the JWT claims structure.
Expand All @@ -14,7 +14,7 @@ type CustomClaims struct {
jwt.RegisteredClaims
}

func main() {
func test() {
// Create claims with username and expiration time
claims := CustomClaims{
Username: "example_user",
Expand Down
9 changes: 5 additions & 4 deletions checkers/go/math_rand.test.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
package main
package golang

import (
"crypto/rand"
"fmt"
// <expect-error> import "math/rand"
"math/rand"

"math/big"
// <expect-error> import "math/rand"
mathrand "math/rand"
"time"
)

Expand All @@ -15,7 +16,7 @@ func unsafeRandomExample(n int) {
// -------------------------
// Weak random number generator
mathrand.Seed(time.Now().UnixNano())
randomNumber := rand.Intn(n) // Not suitable for cryptographic purposes
randomNumber := mathrand.Intn(n) // Not suitable for cryptographic purposes
fmt.Printf("Unsafe random number (math/rand): %d\n", randomNumber)
}

Expand Down
10 changes: 5 additions & 5 deletions checkers/go/sha1_weak_hash.test.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package main
package golang

import (
"crypto/sha1" // Insecure: Weak hash function (SHA-1)
Expand All @@ -7,7 +7,7 @@ import (
"fmt"
)

func main() {
func testSha1WeakHash() {
data := "sensitive_data"

// Insecure: Using SHA-1 (considered broken and vulnerable to collision attacks)
Expand Down Expand Up @@ -43,6 +43,6 @@ func hashWithSHA256(data string) []byte {
// More Secure: SHA-512 hashing (for higher security needs)
func hashWithSHA512(data string) []byte {
hasher := sha512.New()
hashBytes := hasher.Write([]byte(data))
return hashBytes
}
_, _ = hasher.Write([]byte(data))
return hasher.Sum([]byte{})
}
21 changes: 21 additions & 0 deletions checkers/javascript/scope.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// scope resolution implementation for JS and TS files
package javascript

import (
"reflect"

"globstar.dev/analysis"
)

var ScopeAnalyzer = &analysis.Analyzer{
Name: "js-scope",
ResultType: reflect.TypeOf(&analysis.ScopeTree{}),
Run: buildScopeTree,
Language: analysis.LangJs,
}

func buildScopeTree(pass *analysis.Pass) (any, error) {
// Create scope builder for JavaScript
scope := analysis.MakeScopeTree(pass.Analyzer.Language, pass.FileContext.Ast, pass.FileContext.Source)
return scope, nil
}
13 changes: 13 additions & 0 deletions checkers/javascript/testdata/unused-import.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// <expect-error> unused-import: unused import "unused"
import { unused } from './module';

// Should not report used import
import { used } from './module';
console.log(used);

// Should not report used import
import * as namespace from 'module';

function test() {
console.log(namespace);
}
39 changes: 39 additions & 0 deletions checkers/javascript/unused-import.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package javascript

import (
"fmt"

"globstar.dev/analysis"
)

var UnusedImport = &analysis.Analyzer{
Name: "unused-import",
Requires: []*analysis.Analyzer{ScopeAnalyzer},
Run: checkUnusedImports,
Language: analysis.LangJs,
Description: "This checker checks for unused imports in JavaScript code. Unused imports can be removed to reduce the size of the bundle. Unused imports are also a code smell and can indicate that the code is not well-organized.",
Category: analysis.CategoryAntipattern,
Severity: analysis.SeverityInfo,
}

func checkUnusedImports(pass *analysis.Pass) (interface{}, error) {
// Get scope tree from previous analysis
scopeResult := pass.ResultOf[ScopeAnalyzer]
if scopeResult == nil {
return nil, nil
}

scope := scopeResult.(*analysis.ScopeTree)

for _, scope := range scope.ScopeOfNode {
for _, variable := range scope.Variables {
if variable.Kind == analysis.VarKindImport {
if len(variable.Refs) == 0 {
pass.Report(pass, variable.DeclNode, fmt.Sprintf("unused import \"%s\"", variable.Name))
}
}
}
}

return nil, nil
}
1 change: 0 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ go 1.23.2
require (
github.com/go-git/go-git/v5 v5.14.0
github.com/gobwas/glob v0.2.3
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/rs/zerolog v1.33.0
github.com/smacker/go-tree-sitter v0.0.0-20240827094217-dd81d9e9be82
github.com/stretchr/testify v1.10.0
Expand Down
2 changes: 0 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,6 @@ github.com/go-git/go-git/v5 v5.14.0/go.mod h1:Z5Xhoia5PcWA3NF8vRLURn9E5FRhSl7dGj
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
Expand Down
Loading