Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion PATTERNS.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,11 @@ fixearly가 잡는 패턴들의 정본. **점수는 흐릿한 집계, 이 카탈
- **오탐 가드**: **인라인 숫자 리터럴 배열만** 본다. 변수는 런타임 타입을 정적으로 확신할 수 없어 제외(미탐을 감수하고 오탐 0을 택한다)

#### 버린 반환값 (`s.replace(…)` · `arr.concat(…)` 를 대입 없이)
- **탐지**: 문장으로 쓰인 `concat`·`trim*`·`toLowerCase`·`toUpperCase`·`pad*`·`substring`·`repeat`·`toSorted` 류 호출, 그리고 첫 인자가 문자열·정규식 리터럴인 `replace`·`replaceAll`
- **탐지**: 문장으로 쓰인 `concat`·`trim*`·`toLowerCase`·`toUpperCase`·`pad*`·`substring`·`repeat`·`toSorted` 류 호출, 첫 인자가 문자열·정규식 리터럴인 `replace`·`replaceAll`, 그리고 throw 를 빠뜨린 `new XError(…)` 문장
- **왜 버그인가**: 문자열은 불변이고 이 메서드들은 새 값을 돌려줄 뿐이다 — 대입을 빠뜨린 그 줄은 아무것도 안 한다
- **고침**: 결과를 대입한다. 기계적이고, 그 줄이 하려던 일을 테스트로 고정하면 수정 전 실패·수정 후 통과가 바로 나온다
- **오탐 가드**: `this.x()`·같은 파일이 같은 이름을 선언한 자체 메서드(pdf.js `AstIdentifier.toLowerCase`)·magic-string 을 쓰는 파일(rollup·storybook 의 제자리 `replace`/`trim`)·`location`/`router` 의 `replace`(이동)·둘째 인자가 함수인 `replace`(매치를 모으는 순회 관용구)는 뺀다. DOM `normalize()` 와 배열 `reverse`/`sort`/`splice` 는 제자리 변경이라 목록에 없다
- **throw 빠진 에러**: 이름이 `Error`·`Exception` 으로 끝나는 생성자를 문장으로만 부르면 에러가 만들어지고 버려진다. 코퍼스 75곳에서 23건 — node `benchmark/fs` 21곳의 `default: new Error('Invalid type')`(잘못된 type 이 조용히 측정된다)와 playwright `clock.ts:422`(2026-10-10)
- **실적**: [nx #37332](https://github.com/nrwl/nx/pull/37332) — `scam-to-standalone` 이 spec 의 `declarations` 줄을 지우는 `replace` 결과를 버렸다. 24곳 저장소에서 원형을 돌려 걸린 진짜 버그 2건(nx·theia) 중 하나다(2026-10-08)

#### 위치를 진리값으로 (`if (s.indexOf(x))`)
Expand Down
10 changes: 8 additions & 2 deletions bin/fixearly.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1979,6 +1979,12 @@ function analyzeTextbookIssues(ts, fileContents) {
// `location.replace()`·`router.replace()`(이동), 자체 클래스가 같은 이름을 선언한 경우.
// 가드 [FP:replace-as-iterator]: replace 는 첫 인자가 문자열·정규식 리터럴이고 둘째 인자가 함수가
// 아닐 때만 — 콜백으로 매치를 모으는 `str.replaceAll(re, fn)` 은 의도된 순회다(pdf.js util).
// 같은 축: `new XError(…)` 를 만들고 throw 를 빠뜨린 문장. 에러가 아무 일도 안 하고 흐름이 계속된다.
// node benchmark/fs 21곳 `default: new Error('Invalid type')` — 잘못된 type 이 조용히 측정됐다(2026-10-10).
if (ts.isExpressionStatement(node) && ts.isNewExpression(node.expression) &&
ts.isIdentifier(node.expression.expression) && /(Error|Exception)$/.test(node.expression.expression.text)) {
discardedPureCall.push({ file, line: lineOf(node), name: node.expression.expression.text, method: "new" });
}
if (ts.isExpressionStatement(node) && ts.isCallExpression(node.expression) &&
ts.isPropertyAccessExpression(node.expression.expression) && !usesMagicString) {
const call = node.expression;
Expand Down Expand Up @@ -3281,8 +3287,8 @@ if (textbook) {
for (const w of t.statefulRegex.worst) console.log(` ${w.name}.${w.method}() — ${w.file}:${w.line}`);
}
if (t.discardedPureCall?.count > 0) {
console.log(` ⚠ 버린 반환값: ${t.discardedPureCall.count}곳 — 문자열·배열의 순수 메서드 결과를 버립니다 (문자열은 불변이라 그 줄은 아무것도 안 합니다)`);
for (const w of t.discardedPureCall.worst) console.log(` ${w.name}.${w.method}() — ${w.file}:${w.line}`);
console.log(` ⚠ 버린 반환값: ${t.discardedPureCall.count}곳 — 순수 메서드 결과나 throw 빠진 new Error 를 버립니다 (그 줄은 아무것도 안 합니다)`);
for (const w of t.discardedPureCall.worst) console.log(` ${w.method === "new" ? `new ${w.name}` : `${w.name}.${w.method}`}() — ${w.file}:${w.line}`);
}
if (t.indexOfAsBool?.count > 0) {
console.log(` ⚠ 위치를 진리값으로: ${t.indexOfAsBool.count}곳 — indexOf 는 없으면 -1(참)·맨 앞이면 0(거짓)입니다 (includes 나 !== -1)`);
Expand Down
6 changes: 3 additions & 3 deletions bin/selftest.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -251,12 +251,12 @@ if (generatedSrc) {
file: "discarded-pure-call.ts",
key: "discardedPureCall",
label: "버린 반환값",
hit: ["spec", "label"],
hit: ["spec", "label", "ConfigError"],
// assigned 는 대입했고, collector 는 콜백 순회 관용구, location 은 이동,
// token 은 같은 파일이 같은 이름을 선언한 자체 메서드다.
miss: ["assigned", "out", "collector", "location", "token", "sheet", "node", "acc", "vb", "tracked"],
miss: ["assigned", "out", "collector", "location", "token", "sheet", "node", "acc", "vb", "tracked", "ParseError", "Widget"],
// 출력 줄: ` spec.replace() — src/...:7`
namePattern: /^\s{4}([A-Za-z_$][\w$]*)\.\w+\(\) — \S*discarded-pure-call\.ts:/gm,
namePattern: /^\s{4}(?:new )?([A-Za-z_$][\w$]*)(?:\.\w+)?\(\) — \S*discarded-pure-call\.ts:/gm,
},
{
file: "discarded-pure-call-jsx.js",
Expand Down
19 changes: 19 additions & 0 deletions tools/fixtures/discarded-pure-call.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,22 @@ export class Octree {
return this._selection;
}
}

// ConfigError: 에러를 만들고 throw 를 빠뜨렸다 — node benchmark 의 `default: new Error('Invalid type')`.
class ConfigError extends Error {}
export function pickMode(mode: string): number {
switch (mode) {
case 'fast': return 1;
default:
new ConfigError('unknown mode');
}
return 0;
}

// ParseError: throw 했다. Widget: 이름이 Error 가 아닌 생성자는 부수효과용일 수 있다.
class ParseError extends Error {}
class Widget { constructor() { document.body.append('x'); } }
export function parseOrMount(src: string): void {
if (!src) throw new ParseError('empty');
new Widget();
}
Loading