From c17f6ee3494962ed6c5b3fbd1ce1370207717a79 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 2 Jan 2026 20:48:49 +0000 Subject: [PATCH 1/2] Add Laravel Catch Block Rule for PHP - Implemented `LaravelCatchBlockRule` in `analyzers/php/laravel_catch.go` using AST parsing. - Added dependency `github.com/z7zmey/php-parser` for robust PHP parsing. - Updated `PHPAnalyzer` in `analyzers/php/php.go` to include the new rule and process its findings. - The rule detects missing `report()` calls in catch blocks (Critical) and `report()` calls that are not the first statement (Medium) in Laravel app files. - Added comprehensive tests in `analyzers/php/laravel_catch_test.go`. --- analyzers/php/laravel_catch.go | 142 ++++++++++++++++++++++++++++ analyzers/php/laravel_catch_test.go | 128 +++++++++++++++++++++++++ analyzers/php/php.go | 82 ++++++++++------ go.mod | 6 ++ go.sum | 25 +++++ 5 files changed, 355 insertions(+), 28 deletions(-) create mode 100644 analyzers/php/laravel_catch.go create mode 100644 analyzers/php/laravel_catch_test.go diff --git a/analyzers/php/laravel_catch.go b/analyzers/php/laravel_catch.go new file mode 100644 index 0000000..6b14d7d --- /dev/null +++ b/analyzers/php/laravel_catch.go @@ -0,0 +1,142 @@ +package php + +import ( + "code-analyzer/models" + + "github.com/z7zmey/php-parser/node" + "github.com/z7zmey/php-parser/node/expr" + "github.com/z7zmey/php-parser/node/name" + "github.com/z7zmey/php-parser/node/stmt" + "github.com/z7zmey/php-parser/php7" + "github.com/z7zmey/php-parser/walker" +) + +// LaravelCatchBlockRule checks for proper error reporting in try-catch blocks +type LaravelCatchBlockRule struct{} + +func (r *LaravelCatchBlockRule) Name() string { + return "Laravel Catch Block Rule" +} + +// LaravelCatchBlockFinding holds the issues found by the rule +type LaravelCatchBlockFinding struct { + Issues []models.Issue +} + +func (r *LaravelCatchBlockRule) Apply(content string) interface{} { + parser := php7.NewParser([]byte(content), "7.4") + parser.Parse() + + root := parser.GetRootNode() + if root == nil { + return nil + } + + v := &catchVisitor{ + issues: []models.Issue{}, + } + root.Walk(v) + + if len(v.issues) == 0 { + return nil + } + + return LaravelCatchBlockFinding{ + Issues: v.issues, + } +} + +type catchVisitor struct { + issues []models.Issue +} + +// Ensure catchVisitor implements walker.Visitor +var _ walker.Visitor = (*catchVisitor)(nil) + +func (v *catchVisitor) EnterNode(w walker.Walkable) bool { + if n, ok := w.(node.Node); ok { + if catchNode, ok := n.(*stmt.Catch); ok { + v.analyzeCatch(catchNode) + } + } + return true +} + +func (v *catchVisitor) LeaveNode(w walker.Walkable) { + // no-op +} + +func (v *catchVisitor) EnterChildNode(key string, w walker.Walkable) { + // no-op +} + +func (v *catchVisitor) LeaveChildNode(key string, w walker.Walkable) { + // no-op +} + +func (v *catchVisitor) EnterChildList(key string, w walker.Walkable) { + // no-op +} + +func (v *catchVisitor) LeaveChildList(key string, w walker.Walkable) { + // no-op +} + +func (v *catchVisitor) analyzeCatch(n *stmt.Catch) { + // Check statements in the catch block + stmts := n.Stmts + + foundReport := false + isFirst := false + + for i, s := range stmts { + // Look for report(...) call + if isReportCall(s) { + foundReport = true + if i == 0 { + isFirst = true + } + break + } + } + + // Default line number 0 if position not available, but usually it is. + startLine := 0 + if n.GetPosition() != nil { + startLine = n.GetPosition().StartLine + } + + if !foundReport { + v.issues = append(v.issues, models.Issue{ + Description: "Critical: Catch block missing report() call in Laravel app file", + Line: startLine, + Severity: "critical", + }) + } else if !isFirst { + v.issues = append(v.issues, models.Issue{ + Description: "Medium Risk: report() call is not the first statement in catch block", + Line: startLine, + Severity: "medium", + }) + } +} + +func isReportCall(n node.Node) bool { + // We expect an expression statement containing a function call + if exprStmt, ok := n.(*stmt.Expression); ok { + if funcCall, ok := exprStmt.Expr.(*expr.FunctionCall); ok { + // Check function name + if nameNode, ok := funcCall.Function.(*name.Name); ok { + // name.Name parts are parts of the namespace/name + // For "report", it should be a single part "report" + parts := nameNode.Parts + if len(parts) == 1 { + if s, ok := parts[0].(*name.NamePart); ok { + return s.Value == "report" + } + } + } + } + } + return false +} diff --git a/analyzers/php/laravel_catch_test.go b/analyzers/php/laravel_catch_test.go new file mode 100644 index 0000000..ea3e57c --- /dev/null +++ b/analyzers/php/laravel_catch_test.go @@ -0,0 +1,128 @@ +package php + +import ( + "testing" +) + +func TestLaravelCatchBlockRule_Apply(t *testing.T) { + rule := &LaravelCatchBlockRule{} + + tests := []struct { + name string + content string + wantIssues int + wantSeverity string + wantLine int + }{ + { + name: "Critical: No report call", + content: `json(['error' => 'fail']); + } + } +} +`, + wantIssues: 0, + }, + { + name: "Multiple catch blocks", + content: ` 0 { + issue := finding.Issues[0] + if issue.Severity != tt.wantSeverity { + t.Errorf("Expected severity %s, got %s", tt.wantSeverity, issue.Severity) + } + if issue.Line != tt.wantLine { + t.Errorf("Expected line %d, got %d", tt.wantLine, issue.Line) + } + } + }) + } +} diff --git a/analyzers/php/php.go b/analyzers/php/php.go index d09813f..267eef2 100644 --- a/analyzers/php/php.go +++ b/analyzers/php/php.go @@ -23,6 +23,7 @@ func NewPHPAnalyzer() *PHPAnalyzer { return &PHPAnalyzer{ rules: []analyzers.Rule{ &CommentedFunctionsRule{}, + &LaravelCatchBlockRule{}, }, } } @@ -57,10 +58,11 @@ func (a *PHPAnalyzer) Run(config analyzers.Config) ([]models.Issue, error) { analysis := a.analyzeFile(path) if analysis != nil { - if analysis.CommentedFunctions < config.MinValue { + // Skip if below threshold AND no other issues + if analysis.CommentedFunctions < config.MinValue && len(analysis.Issues) == 0 { return nil } - if config.MinRatio > 0 && analysis.CommentRatio < config.MinRatio { + if config.MinRatio > 0 && analysis.CommentRatio < config.MinRatio && len(analysis.Issues) == 0 { return nil } @@ -111,43 +113,67 @@ func (a *PHPAnalyzer) analyzeFile(path string) *models.PHPFileAnalysis { if err != nil { return nil } + contentStr := string(content) + + var analysis *models.PHPFileAnalysis + var allIssues []models.Issue // Apply commented functions rule - rule := &CommentedFunctionsRule{} - finding := rule.Apply(string(content)) + cfRule := &CommentedFunctionsRule{} + if finding := cfRule.Apply(contentStr); finding != nil { + result := finding.(CommentedFunctionsFinding) + + totalBytes := len(content) + commentedBytes := len(result.CommentedList) * 20 // rough estimate + ratio := 0.0 + if len(result.AllFunctions) > 0 { + ratio = float64(len(result.CommentedList)) / float64(len(result.AllFunctions)) * 100 + } - if finding == nil { - return nil + // Set path for issues + for i := range result.Issues { + result.Issues[i].Path = path + } + allIssues = append(allIssues, result.Issues...) + + analysis = &models.PHPFileAnalysis{ + Path: path, + TotalFunctions: len(result.AllFunctions), + CommentedFunctions: len(result.CommentedList), + FunctionList: result.AllFunctions, + CommentedList: result.CommentedList, + CommentRatio: ratio, + TotalBytes: totalBytes, + CommentedBytes: commentedBytes, + } } - result := finding.(CommentedFunctionsFinding) - if len(result.CommentedList) == 0 { - return nil + // Apply Laravel Catch Block Rule + if strings.Contains(path, "app/") { + lcbRule := &LaravelCatchBlockRule{} + if finding := lcbRule.Apply(contentStr); finding != nil { + result := finding.(LaravelCatchBlockFinding) + for i := range result.Issues { + result.Issues[i].Path = path + } + allIssues = append(allIssues, result.Issues...) + } } - // Set path for issues - for i := range result.Issues { - result.Issues[i].Path = path + if analysis == nil && len(allIssues) == 0 { + return nil } - totalBytes := len(content) - commentedBytes := len(result.CommentedList) * 20 // rough estimate - ratio := 0.0 - if len(result.AllFunctions) > 0 { - ratio = float64(len(result.CommentedList)) / float64(len(result.AllFunctions)) * 100 + if analysis == nil { + // Create a basic analysis object if we only have other issues + analysis = &models.PHPFileAnalysis{ + Path: path, + TotalBytes: len(content), + } } - return &models.PHPFileAnalysis{ - Path: path, - TotalFunctions: len(result.AllFunctions), - CommentedFunctions: len(result.CommentedList), - FunctionList: result.AllFunctions, - CommentedList: result.CommentedList, - CommentRatio: ratio, - TotalBytes: totalBytes, - CommentedBytes: commentedBytes, - Issues: result.Issues, - } + analysis.Issues = allIssues + return analysis } func (a *PHPAnalyzer) printResults(results []models.PHPFileAnalysis, totalFunctions, totalCommented int) { diff --git a/go.mod b/go.mod index fd41f5c..d3eea1e 100644 --- a/go.mod +++ b/go.mod @@ -3,3 +3,9 @@ module code-analyzer go 1.24 require gopkg.in/yaml.v3 v3.0.1 + +require ( + github.com/pkg/profile v1.4.0 // indirect + github.com/yookoala/realpath v1.0.0 // indirect + github.com/z7zmey/php-parser v0.7.2 // indirect +) diff --git a/go.sum b/go.sum index a62c313..81629da 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,29 @@ +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/profile v1.4.0 h1:uCmaf4vVbWAOZz36k1hrQD7ijGRzLwaME8Am/7a4jZI= +github.com/pkg/profile v1.4.0/go.mod h1:NWz/XGvpEW1FyYQ7fCx4dqYBLlfTcE+A9FLAkNKqjFE= +github.com/yookoala/realpath v1.0.0 h1:7OA9pj4FZd+oZDsyvXWQvjn5oBdcHRTV44PpdMSuImQ= +github.com/yookoala/realpath v1.0.0/go.mod h1:gJJMA9wuX7AcqLy1+ffPatSCySA1FQ2S8Ya9AIoYBpE= +github.com/z7zmey/php-parser v0.7.2 h1:hnSNxn6tqK3n8JrevuBRVSI856v4yUJWgTonUVps5zA= +github.com/z7zmey/php-parser v0.7.2/go.mod h1:r03mwVJvNhQKrTqKFzK0MIepU1uO62Z0p9ES3A7KTu4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200308013534-11ec41452d41/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= From 67f488a9014f818de5939e59505b7a7cab3d7ae0 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 3 Jan 2026 01:20:36 +0000 Subject: [PATCH 2/2] Add Laravel Catch Block Rule for PHP - Implemented `LaravelCatchBlockRule` in `analyzers/php/laravel_catch.go` using AST parsing. - Added dependency `github.com/z7zmey/php-parser` for robust PHP parsing. - Updated `PHPAnalyzer` in `analyzers/php/php.go` to include the new rule and process its findings. - The rule detects missing `report()` calls in catch blocks (Critical) and `report()` calls that are not the first statement (Medium) in Laravel app files. - Added metrics `CatchBlocksMissingReport` and `CatchBlocksMisplacedReport` to `PHPFileAnalysis` model and output. - Added comprehensive tests in `analyzers/php/laravel_catch_test.go`. --- analyzers/php/laravel_catch.go | 14 +++++++++++--- analyzers/php/php.go | 12 ++++++++++++ models/models.go | 3 +++ 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/analyzers/php/laravel_catch.go b/analyzers/php/laravel_catch.go index 6b14d7d..1ec0f68 100644 --- a/analyzers/php/laravel_catch.go +++ b/analyzers/php/laravel_catch.go @@ -20,7 +20,9 @@ func (r *LaravelCatchBlockRule) Name() string { // LaravelCatchBlockFinding holds the issues found by the rule type LaravelCatchBlockFinding struct { - Issues []models.Issue + Issues []models.Issue + MissingReport int + MisplacedReport int } func (r *LaravelCatchBlockRule) Apply(content string) interface{} { @@ -42,12 +44,16 @@ func (r *LaravelCatchBlockRule) Apply(content string) interface{} { } return LaravelCatchBlockFinding{ - Issues: v.issues, + Issues: v.issues, + MissingReport: v.missingReport, + MisplacedReport: v.misplacedReport, } } type catchVisitor struct { - issues []models.Issue + issues []models.Issue + missingReport int + misplacedReport int } // Ensure catchVisitor implements walker.Visitor @@ -107,12 +113,14 @@ func (v *catchVisitor) analyzeCatch(n *stmt.Catch) { } if !foundReport { + v.missingReport++ v.issues = append(v.issues, models.Issue{ Description: "Critical: Catch block missing report() call in Laravel app file", Line: startLine, Severity: "critical", }) } else if !isFirst { + v.misplacedReport++ v.issues = append(v.issues, models.Issue{ Description: "Medium Risk: report() call is not the first statement in catch block", Line: startLine, diff --git a/analyzers/php/php.go b/analyzers/php/php.go index 267eef2..d0a596e 100644 --- a/analyzers/php/php.go +++ b/analyzers/php/php.go @@ -149,10 +149,13 @@ func (a *PHPAnalyzer) analyzeFile(path string) *models.PHPFileAnalysis { } // Apply Laravel Catch Block Rule + var catchMissing, catchMisplaced int if strings.Contains(path, "app/") { lcbRule := &LaravelCatchBlockRule{} if finding := lcbRule.Apply(contentStr); finding != nil { result := finding.(LaravelCatchBlockFinding) + catchMissing = result.MissingReport + catchMisplaced = result.MisplacedReport for i := range result.Issues { result.Issues[i].Path = path } @@ -172,6 +175,9 @@ func (a *PHPAnalyzer) analyzeFile(path string) *models.PHPFileAnalysis { } } + analysis.CatchBlocksMissingReport = catchMissing + analysis.CatchBlocksMisplacedReport = catchMisplaced + analysis.Issues = allIssues return analysis } @@ -198,6 +204,12 @@ func (a *PHPAnalyzer) printResults(results []models.PHPFileAnalysis, totalFuncti result.TotalFunctions, result.CommentedFunctions, result.CommentRatio) + + // Optional: Print catch block warnings if present + if result.CatchBlocksMissingReport > 0 || result.CatchBlocksMisplacedReport > 0 { + fmt.Printf(" ⚠️ Catch Blocks: %d missing report(), %d misplaced\n", + result.CatchBlocksMissingReport, result.CatchBlocksMisplacedReport) + } } fmt.Println() diff --git a/models/models.go b/models/models.go index 6d8619e..47d4dff 100644 --- a/models/models.go +++ b/models/models.go @@ -60,6 +60,9 @@ type PHPFileAnalysis struct { TotalBytes int `json:"total_bytes"` CommentedBytes int `json:"commented_bytes"` Issues []Issue `json:"issues"` + // Laravel Catch Block metrics + CatchBlocksMissingReport int `json:"catch_blocks_missing_report,omitempty"` + CatchBlocksMisplacedReport int `json:"catch_blocks_misplaced_report,omitempty"` } // PHPAnalysisReport represents the complete PHP analysis report