Harden lint cancellation handling

This commit is contained in:
Snider 2026-04-16 06:32:41 +01:00
parent 47c0f56863
commit 49bf3c36f4
3 changed files with 147 additions and 1 deletions

View file

@ -206,6 +206,15 @@ func (adapter CommandAdapter) Run(ctx context.Context, input RunInput, files []s
}
}
if err := runContext.Err(); err != nil {
if errors.Is(err, context.DeadlineExceeded) {
result.Tool.Status = "timeout"
} else {
result.Tool.Status = "canceled"
}
return result
}
if adapter.parseOutput != nil && output != "" {
result.Findings = adapter.parseOutput(adapter.name, adapter.category, output)
}
@ -300,7 +309,7 @@ func (CatalogAdapter) Category() string { return "correctness" }
func (CatalogAdapter) Fast() bool { return true }
func (CatalogAdapter) Run(_ context.Context, input RunInput, files []string) AdapterResult {
func (CatalogAdapter) Run(ctx context.Context, input RunInput, files []string) AdapterResult {
startedAt := time.Now()
result := AdapterResult{
Tool: ToolRun{
@ -346,6 +355,9 @@ func (CatalogAdapter) Run(_ context.Context, input RunInput, files []string) Ada
var findings []Finding
if len(files) > 0 {
for _, file := range files {
if err := ctx.Err(); err != nil {
break
}
scanPath := file
if !filepath.IsAbs(scanPath) {
scanPath = filepath.Join(input.Path, file)
@ -357,9 +369,22 @@ func (CatalogAdapter) Run(_ context.Context, input RunInput, files []string) Ada
findings = append(findings, fileFindings...)
}
} else {
if ctx.Err() != nil {
result.Tool.Status = "canceled"
result.Tool.Duration = time.Since(startedAt).Round(time.Millisecond).String()
return result
}
findings, _ = scanner.ScanDir(input.Path)
}
if err := ctx.Err(); err != nil {
result.Tool.Status = "canceled"
result.Tool.Duration = time.Since(startedAt).Round(time.Millisecond).String()
result.Tool.Findings = len(findings)
result.Findings = findings
return result
}
for index := range findings {
rule := catalog.ByID(findings[index].RuleID)
findings[index].Tool = "catalog"

View file

@ -144,6 +144,9 @@ func (service *Service) Run(ctx context.Context, input RunInput) (Report, error)
var toolRuns []ToolRun
for _, adapter := range selectedAdapters {
if err := ctx.Err(); err != nil {
break
}
if input.Hook && !adapter.Fast() {
toolRuns = append(toolRuns, ToolRun{
Name: adapter.Name(),

View file

@ -566,6 +566,38 @@ func TestServiceTools_EmptyInventoryReturnsEmptySlice(t *testing.T) {
assert.Empty(t, tools)
}
func TestServiceRun_Good_StopsDispatchingAfterContextCancel(t *testing.T) {
dir := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(dir, "composer.json"), []byte("{\n \"name\": \"example/test\"\n}\n"), 0o644))
require.NoError(t, os.MkdirAll(filepath.Join(dir, ".core"), 0o755))
require.NoError(t, os.WriteFile(filepath.Join(dir, ".core", "lint.yaml"), []byte(`lint:
php:
- first
- second
`), 0o644))
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
var secondRan bool
svc := &Service{adapters: []Adapter{
cancellingAdapter{name: "first", cancel: cancel},
recordingAdapter{name: "second", ran: &secondRan},
}}
report, err := svc.Run(ctx, RunInput{
Path: dir,
Lang: "php",
FailOn: "warning",
})
require.NoError(t, err)
require.Len(t, report.Tools, 1)
assert.Equal(t, "first", report.Tools[0].Name)
assert.False(t, secondRan)
assert.Empty(t, report.Findings)
}
type shortcutAdapter struct {
name string
category string
@ -599,6 +631,92 @@ func (adapter shortcutAdapter) Run(_ context.Context, _ RunInput, _ []string) Ad
}
}
type recordingAdapter struct {
name string
ran *bool
}
func (adapter recordingAdapter) Name() string { return adapter.name }
func (adapter recordingAdapter) Available() bool { return true }
func (adapter recordingAdapter) Languages() []string { return []string{"php"} }
func (adapter recordingAdapter) Command() string { return adapter.name }
func (adapter recordingAdapter) Entitlement() string { return "" }
func (adapter recordingAdapter) RequiresEntitlement() bool { return false }
func (adapter recordingAdapter) MatchesLanguage(languages []string) bool {
for _, language := range languages {
if language == "php" {
return true
}
}
return false
}
func (adapter recordingAdapter) Category() string { return "correctness" }
func (adapter recordingAdapter) Fast() bool { return true }
func (adapter recordingAdapter) Run(_ context.Context, _ RunInput, _ []string) AdapterResult {
if adapter.ran != nil {
*adapter.ran = true
}
return AdapterResult{
Tool: ToolRun{
Name: adapter.name,
Status: "passed",
Duration: "0s",
},
}
}
type cancellingAdapter struct {
name string
cancel context.CancelFunc
}
func (adapter cancellingAdapter) Name() string { return adapter.name }
func (adapter cancellingAdapter) Available() bool { return true }
func (adapter cancellingAdapter) Languages() []string { return []string{"php"} }
func (adapter cancellingAdapter) Command() string { return adapter.name }
func (adapter cancellingAdapter) Entitlement() string { return "" }
func (adapter cancellingAdapter) RequiresEntitlement() bool { return false }
func (adapter cancellingAdapter) MatchesLanguage(languages []string) bool {
for _, language := range languages {
if language == "php" {
return true
}
}
return false
}
func (adapter cancellingAdapter) Category() string { return "correctness" }
func (adapter cancellingAdapter) Fast() bool { return true }
func (adapter cancellingAdapter) Run(_ context.Context, _ RunInput, _ []string) AdapterResult {
if adapter.cancel != nil {
adapter.cancel()
}
return AdapterResult{
Tool: ToolRun{
Name: adapter.name,
Status: "passed",
Duration: "0s",
},
}
}
type duplicateAdapter struct {
name string
finding Finding