Skip to content

Commit d9ba7d2

Browse files
dancinlifeclaude
andcommitted
feat(harness): R19 SILENT EXIT 공통 강제 추가
check_silent_exit — .hexa/.sh/.py/.ts/.js 대상 - exit(1) 직전 5줄에 println/eprintln/>&2/stderr 없음 - catch 블록 body 에 print/stderr 참조 없음 (silent swallow) - 예외: // @allow-silent-exit / // @allow-silent-catch 라인 주석 자기 참조 방지: shared/harness/* 는 exec 폴백 의도적 silent 이므로 스킵. severity: error (commit 차단) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent a65f3df commit d9ba7d2

1 file changed

Lines changed: 28 additions & 0 deletions

File tree

shared/harness/lint.hexa

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,28 @@ fn check_json(path: string) -> string {
216216
return ""
217217
}
218218

219+
// ─── Check R19: SILENT EXIT 금지 — exit(1) 전 stderr 출력 필수 + catch 로그 필수 ───
220+
// 탐지 2종:
221+
// (1) exit(1) 직전 5줄에 println/eprintln/>&2/stderr 없음
222+
// (2) catch 블록 body 에 print/e.* 참조 없음 (silent swallow)
223+
// 예외: 라인 주석 // @allow-silent-exit 또는 // @allow-silent-catch
224+
fn check_silent_exit(path: string) -> string {
225+
let e = ext_of(path)
226+
if e != ".hexa" && e != ".sh" && e != ".py" && e != ".ts" && e != ".js" { return "" }
227+
// 하네스 자신은 exec 폴백 의도적 silent — 스킵
228+
if path.starts_with("shared/harness/") { return "" }
229+
let full = REPO_ROOT + "/" + path
230+
// (1) exit(1) silent
231+
let awk1 = "awk '/exit[[:space:]]*\\([[:space:]]*1[[:space:]]*\\)/ && $0 !~ /@allow-silent-exit/ { has=0; for(i=NR-5;i<NR;i++){if(lines[i] ~ /println|eprintln|>&2|stderr|fprintf[[:space:]]*\\(stderr/) has=1}; if(!has) c++ } { lines[NR]=$0 } END{print c+0}'"
232+
let silent_exits = to_int(_str(exec(awk1 + " '" + full + "' 2>/dev/null || echo 0")).trim())
233+
// (2) silent catch — block 안에 print 또는 error var 참조 없음
234+
let awk2 = "awk '/catch[[:space:]]+[a-zA-Z_][a-zA-Z0-9_]*[[:space:]]*\\{/ && $0 !~ /@allow-silent-catch/ { start=NR; depth=0; buf=\"\"; for(i=1;i<=length($0);i++){c=substr($0,i,1); if(c==\"{\")depth++; else if(c==\"}\")depth--}; buf=$0; if(depth==0){ if(buf !~ /println|eprintln|>&2|stderr/) n++ ; next } in_b=1; next } in_b { buf=buf $0; for(i=1;i<=length($0);i++){c=substr($0,i,1); if(c==\"{\")depth++; else if(c==\"}\")depth--}; if(depth==0){ if(buf !~ /println|eprintln|>&2|stderr/) n++; in_b=0 } } END{print n+0}'"
235+
let silent_catches = to_int(_str(exec(awk2 + " '" + full + "' 2>/dev/null || echo 0")).trim())
236+
let total = silent_exits + silent_catches
237+
if total == 0 { return "" }
238+
return "R19|error|SILENT EXIT — exit(1) 앞 stderr 없음 " + to_string(silent_exits) + "건 + silent catch " + to_string(silent_catches) + "건 (// @allow-silent-exit/catch 로 예외)"
239+
}
240+
219241
// ─── Check 9: harness 수정 가드 — commit 메시지 승인 마커 필수 ───
220242
// shared/harness/ 아래 .hexa/.json/.jsonl/.md 수정 시 commit 메시지에
221243
// "(harness):" prefix 또는 "harness-approved" 포함 필요.
@@ -402,6 +424,12 @@ for f in files {
402424
// check_harness_modification: shared/harness/ 수정 시 승인 마커 필요
403425
let v9 = check_harness_modification(path)
404426
if v9 != "" { violations = violations.push(path + "|" + v9) }
427+
428+
// check_silent_exit (R19): .hexa/.sh/.py/.ts/.js 대상
429+
if e == ".hexa" || e == ".sh" || e == ".py" || e == ".ts" || e == ".js" {
430+
let v10 = check_silent_exit(path)
431+
if v10 != "" { violations = violations.push(path + "|" + v10) }
432+
}
405433
}
406434

407435
let pass = len(violations) == 0

0 commit comments

Comments
 (0)