arethetypeswrong / arethetypeswrong/arethetypeswrong.github.io

`--format json` still truncated through a pipe when the exit code is non-zero (#184)

Đang mở
#279 0 bình luận 0 reaction 0 người được giao Xem trên GitHub
Ngôn ngữ chính
TypeScript
Star
1.6k
Fork
65
Chỉ số merge pull request
Không có pull request nào được merge trong 30 ngày

Mô tả

## Summary

`--format json` output is still truncated when stdout is a pipe, on `@arethetypeswrong/cli` 0.18.4. This is #184, which was closed but remains reproducible.

The truncation is governed by the **exit code, not the payload size**: an identically-sized payload from a *passing* package traverses the same pipe intact. Only a run that reports problems (exit code ≠ 0) truncates.

That detail matters for why this was believed fixed: the streaming `write()` helper appears to have been introduced to address #184 — its comment says as much — but it doesn't achieve the flush it was written for, and verifying it against a clean package would have shown it working.

## Reproduction

Two packages, 8 entry points each, near-identical JSON payloads. The only difference: one has findings.

- passing: each entry point is valid ESM
- failing: same shape, but each entry point's `.js` body is `module.exports.value = 1;` (CJS syntax in an ESM package)

```bash
attw pkg.tgz --profile esm-only --format json | cat # through a pipe
attw pkg.tgz --profile esm-only --format json > out # to a file
```

| | exit | to file | through pipe | |
| ------- | ---- | --------- | ------------- | ----------- |
| PASSING | 0 | 152,795 B | 152,795 B | parses |
| FAILING | 1 | 157,642 B | **65,536 B** | parse fails |

65,536 is exactly the pipe buffer capacity (the reporter of #184 saw 98,304 — the figure varies by platform, the mechanism doesn't).

## Root cause

`write()` resolves on the **source** `Readable`'s `end` event, not on the destination draining:

```js
export async function write(data, out) {
return new Promise((resolve, reject) => {
const stream = new Readable({ read() { this.push(data); this.push("\n"); this.push(null); } });
stream.on("data", (chunk) => {
out.write(chunk); // <- return value ignored; may merely buffer
});
stream.on("end", () => {
resolve(); // <- resolves when the SOURCE is exhausted
});
out.on("error", (err) => reject(err));
});
}
```

`out.write()` on a pipe is asynchronous — it returns `false` once past the high-water mark, leaving the data queued. `end` fires when the `Readable` has been fully consumed, which says nothing about whether **stdout** has flushed. So `write()` resolves with bytes still pending. Functionally this is equivalent to a bare `out.write(data)`; the stream indirection doesn't add a flush guarantee.

Then, in the JSON branch of `index.ts`:

```js
await write(JSON.stringify(result, undefined, 2), out);
// ...
const exitCode = getExitCode(analysis, opts);
if (exitCode) {
process.exit(exitCode); // <- discards the still-buffered tail
}
return;
```

`process.exit()` terminates without flushing pending async writes.

This accounts for every observed behavior:

- **Non-zero exit** → `process.exit()` → truncated.
- **Zero exit** → falls through to `return` → Node drains stdout on natural exit → intact.
- **Non-JSON formats** → set `process.exitCode` rather than calling `process.exit()` → also drain. Which is why only `--format json` is affected.

## Impact

Any consumer reading `attw --format json` through a pipe gets invalid JSON precisely for the packages that *have* findings and more than a handful of entry points. This includes `child_process.spawnSync`/`execSync`, which capture output via pipes — so it hits programmatic consumers, not just interactive `| jq`.

The failure mode is unkind: it strikes only when there are problems **and** the package is large, so fixtures that are small or clean will pass while real-world usage breaks.

## Suggested fix

Either resolves it; they're complementary.

**1. Don't `process.exit()` on the JSON path** — mirror what the human-readable path already does:

```js
process.exitCode = exitCode;
return;
```

Smallest change, consistent with the existing branch, and sufficient on its own.

**2. Make `write()` actually await the flush** — repairs the primitive for every caller:

```js
export function write(data, out) {
return new Promise((resolve, reject) => {
out.write(data + "\n", (err) => (err ? reject(err) : resolve()));
});
}
```

`stream.write(chunk, cb)` invokes `cb` once the chunk has been flushed, so awaiting it before exiting is sufficient.

## Environment

- `@arethetypeswrong/cli` 0.18.4
- Node v24.14.1
- macOS (Darwin 25.5.0)

Hướng dẫn đóng góp

Chưa lập chỉ mục được hướng dẫn đóng góp cho kho mã nguồn này

Hướng nghiên cứu

Bắt đầu trong nhánh JSON của index.ts và tìm helper write() được mô tả trong issue. Chạy các lệnh được cung cấp cho package bị lỗi qua một pipe và vào một tệp, sau đó kiểm tra xem việc hoàn tất output có được chờ trước khi thoát với giá trị khác 0 hay không. Hoàn thành khi trường hợp bị lỗi tạo ra JSON đầy đủ, có thể phân tích được qua một pipe mà không gây hồi quy cho trường hợp thành công.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Đánh giá

Công nghệ
node.js, typescript
Lĩnh vực
cli
Loại issue
Lỗi
Độ khó
3/5
Thời gian dự kiến
1-2 ngày
Mức độ hoạt động
Ít trao đổi
Độ rõ ràng
Đặc tả rõ ràng
Mức phù hợp với người mới
72/100

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.