Skip to content

Commit 2df359e

Browse files
feat: add Requires field to the Analyzer struct (#184)
Also add a scope tree checker that uses the `Requires` field to pass data to another checker. This new checker detects unused imports in JS files.
1 parent 17d8c32 commit 2df359e

File tree

14 files changed

+137
-48
lines changed

14 files changed

+137
-48
lines changed

analysis/analyzer.go

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
"os"
66
"path/filepath"
7+
"reflect"
78
"slices"
89

910
sitter "github.com/smacker/go-tree-sitter"
@@ -50,14 +51,19 @@ type Analyzer struct {
5051
Category Category
5152
Severity Severity
5253
Language Language
53-
Run func(*Pass) (interface{}, error)
54+
Requires []*Analyzer
55+
Run func(*Pass) (any, error)
56+
ResultType reflect.Type
5457
}
5558

5659
type Pass struct {
5760
Analyzer *Analyzer
5861
FileContext *ParseResult
5962
Files []*ParseResult
63+
ResultOf map[*Analyzer]any
6064
Report func(*Pass, *sitter.Node, string)
65+
// TODO (opt): the cache should ideally not be stored in-memory
66+
ResultCache map[*Analyzer]map[*ParseResult]any
6167
}
6268

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

97+
func findAnalyzers(analyzer *Analyzer) []*Analyzer {
98+
analyzers := []*Analyzer{}
99+
for _, req := range analyzer.Requires {
100+
analyzers = append(analyzers, findAnalyzers(req)...)
101+
}
102+
analyzers = append(analyzers, analyzer)
103+
return analyzers
104+
}
105+
91106
func RunAnalyzers(path string, analyzers []*Analyzer, fileFilter func(string) bool) ([]*Issue, error) {
92107
raisedIssues := []*Issue{}
93108
langAnalyzerMap := make(map[Language][]*Analyzer)
109+
94110
for _, analyzer := range analyzers {
95-
langAnalyzerMap[analyzer.Language] = append(langAnalyzerMap[analyzer.Language], analyzer)
111+
langAnalyzerMap[analyzer.Language] = append(langAnalyzerMap[analyzer.Language], findAnalyzers(analyzer)...)
96112
}
97113

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

140156
for lang, analyzers := range langAnalyzerMap {
141-
for _, analyzer := range analyzers {
142-
allFiles := trees[lang]
143-
if len(allFiles) == 0 {
144-
continue
145-
}
157+
pass := &Pass{
158+
Files: trees[lang],
159+
Report: reportFunc,
160+
ResultOf: make(map[*Analyzer]any),
161+
ResultCache: make(map[*Analyzer]map[*ParseResult]any),
162+
}
163+
164+
for _, file := range pass.Files {
165+
pass.FileContext = file
166+
for _, analyzer := range analyzers {
167+
pass.Analyzer = analyzer
146168

147-
for _, file := range allFiles {
148-
pass := &Pass{
149-
Analyzer: analyzer,
150-
FileContext: file,
151-
Files: trees[lang],
152-
Report: reportFunc,
169+
if len(pass.Files) == 0 {
170+
continue
153171
}
154172

155-
_, err := analyzer.Run(pass)
173+
result, err := analyzer.Run(pass)
156174
if err != nil {
157175
return raisedIssues, err
158176
}
177+
178+
pass.ResultOf[analyzer] = result
179+
if _, ok := pass.ResultCache[analyzer]; !ok {
180+
pass.ResultCache[analyzer] = make(map[*ParseResult]any)
181+
}
182+
pass.ResultCache[analyzer][file] = result
159183
}
160184
}
161185
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ var ScopeNodes = []string{
3030
"for_statement",
3131
"for_in_statement",
3232
"for_of_statement",
33+
"program",
3334
}
3435

3536
func (ts *TsScopeBuilder) NodeCreatesScope(node *sitter.Node) bool {

checkers/go/cgi_import.test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
package main
1+
package golang
22

33
import (
44
// <expect-error> usage of cgi package
5-
"net/http/cgi"
65
"fmt"
76
"net/http"
7+
"net/http/cgi"
88
)
99

1010
func cgi_test() {
Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,24 @@
1-
// <expect-error>
2-
fmt.Print()
3-
4-
// <expect-error>
5-
fmt.Println()
6-
7-
8-
package main
1+
package golang
92

103
import (
114
"fmt"
12-
"logger"
5+
"log"
136
)
147

15-
func main() {
8+
func testFmtInProd() {
169
// <expect-error>
17-
fmt.Print("Hello, ")
10+
fmt.Print("Hello, ")
1811
// <expect-error>
19-
fmt.Print("World!")
12+
fmt.Print("World!")
2013
// <expect-error>
21-
fmt.Println("Hello, World!")
14+
fmt.Println("Hello, World!")
2215
// <expect-error>
23-
fmt.Println("Line 1")
16+
fmt.Println("Line 1")
2417
// <expect-error>
2518
fmt.Printf("Name: %s, Age: %d\n", name, age)
2619

2720
// Safe
28-
logger.Info("Hello, World!")
21+
log.Println("Hello, World!")
2922
//Safe
30-
logger.Debug("Hello, World!")
23+
log.Println("Hello, World!")
3124
}

checkers/go/http_file_server.test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package main
1+
package golang
22

33
import (
44
"log"

checkers/go/jwt_none_algorithm.test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
package main
1+
package golang
22

33
import (
44
"fmt"
55
"os"
66
"time"
77

8-
"github.com/golang-jwt/jwt/v5"
8+
"jwt"
99
)
1010

1111
// CustomClaims defines the JWT claims structure.
@@ -14,7 +14,7 @@ type CustomClaims struct {
1414
jwt.RegisteredClaims
1515
}
1616

17-
func main() {
17+
func test() {
1818
// Create claims with username and expiration time
1919
claims := CustomClaims{
2020
Username: "example_user",

checkers/go/math_rand.test.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
package main
1+
package golang
22

33
import (
44
"crypto/rand"
55
"fmt"
6-
// <expect-error> import "math/rand"
7-
"math/rand"
6+
87
"math/big"
8+
// <expect-error> import "math/rand"
9+
mathrand "math/rand"
910
"time"
1011
)
1112

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

checkers/go/sha1_weak_hash.test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package main
1+
package golang
22

33
import (
44
"crypto/sha1" // Insecure: Weak hash function (SHA-1)
@@ -7,7 +7,7 @@ import (
77
"fmt"
88
)
99

10-
func main() {
10+
func testSha1WeakHash() {
1111
data := "sensitive_data"
1212

1313
// Insecure: Using SHA-1 (considered broken and vulnerable to collision attacks)
@@ -43,6 +43,6 @@ func hashWithSHA256(data string) []byte {
4343
// More Secure: SHA-512 hashing (for higher security needs)
4444
func hashWithSHA512(data string) []byte {
4545
hasher := sha512.New()
46-
hashBytes := hasher.Write([]byte(data))
47-
return hashBytes
48-
}
46+
_, _ = hasher.Write([]byte(data))
47+
return hasher.Sum([]byte{})
48+
}

checkers/javascript/scope.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// scope resolution implementation for JS and TS files
2+
package javascript
3+
4+
import (
5+
"reflect"
6+
7+
"globstar.dev/analysis"
8+
)
9+
10+
var ScopeAnalyzer = &analysis.Analyzer{
11+
Name: "js-scope",
12+
ResultType: reflect.TypeOf(&analysis.ScopeTree{}),
13+
Run: buildScopeTree,
14+
Language: analysis.LangJs,
15+
}
16+
17+
func buildScopeTree(pass *analysis.Pass) (any, error) {
18+
// Create scope builder for JavaScript
19+
scope := analysis.MakeScopeTree(pass.Analyzer.Language, pass.FileContext.Ast, pass.FileContext.Source)
20+
return scope, nil
21+
}

0 commit comments

Comments
 (0)