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
2 changes: 2 additions & 0 deletions FALSE-POSITIVES.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@
| `for-in-needs-array-evidence` | 배열에 for...in | 객체에 쓰는 `for...in` 은 정상인데 같이 잡았다 | 코퍼스 검증 | 배열이라는 증거(리터럴·`map`·`filter`·`split`)가 있는 변수만 | `for-in-array.ts` |
| `for-in-name-collision` | 배열에 for...in | 증거를 파일 스코프로 모으는 탓에, 다른 함수에서 같은 이름이 배열로 선언돼 있으면 객체를 도는 정상 `for...in` 까지 끌려왔다 | mongoose `schema.js:2396`(객체 인덱스 스펙) 이 `schema.js:1005` 의 동명 배열 때문에 잡혔다 — 그 저장소 후보 2건 중 1건 | 같은 이름이 파일에서 두 번 이상 선언되면(파라미터 포함) 포기한다 — `same-name-twice` 와 같은 정책이고 미탐을 산다 | `for-in-array.ts` |
| `throw-path-runs-once` | O(n²) 배열 조회 | 반복대상과 같은 배열을 스캔해 '같이 자람'으로 떴지만, 그 스캔은 throw 하는 블록 안의 오류 메시지 조립이었다 — 실패할 때 한 번 돈다 | jest `jest-config/src/index.ts:283·285` (`ensureNoDuplicateConfigs`) — 그 저장소 `[같이자람]` 2건이 전부 이것. webpack `ExportsInfo.js:707` 도 호출자 쪽에서 같은 이유였다(파일 밖이라 이 가드로는 못 잡는다) | 수신자에서 루프까지 올라가며 `throw` 문을 가진 블록이 있으면 `throw-path` 컷 + coGrows 해제 | `quadratic-co-growth.ts` |
| `quad-loop-header-once` | O(n²) 배열 조회 | for-of 반복 대상 식·for 초기화식 안의 스캔은 루프 진입 때 한 번 평가된다 | elysia `src/index.ts:8039` `for (const type of types.some(…) ? […] : types)` (10-11, 74곳 표본 53개 손검증) | for-of/for-in 의 `expression` 과 for 의 `initializer` 는 바깥 루프 문맥으로 걷는다 | `quadratic-gates.ts` |
| `quad-object-values-enum` | O(n²) 배열 조회 | `Object.values(Enum)` 바깥은 enum·상수 객체의 고정 목록이다 — 루트가 `Object` 라 데이터로 봤다 | immich `repositories/integrity.repository.ts:50` `Object.values(IntegrityReport).map(… counts.find …)` (10-11) | `Object.values/keys/entries(X)` 에서 X 가 대문자 시작이거나 모듈 상수면 `const-outer` 컷 | `quadratic-gates.ts` |
| `fill-domain-api` | 공유 참조 fill | `page.fill(selector, {...})` 처럼 도메인 API 의 `.fill()` 을 `Array.prototype.fill` 로 봤다 | playwright **3곳** | 수신자가 '배열을 만드는 표현'일 때만 | `shared-ref-fill.ts` |
| `map-is-not-a-loop` | 루프 안 new RegExp | 모듈 로드 시 1회 도는 `CACHED = entries.map(... new RegExp ...)` 를 재컴파일로 봤다 | 코퍼스 검증 | 진짜 반복문(`for`/`while`) 안에서만 | `regex-in-loop.ts` |
| `derived-copy` | 루프 안 인덱스 재구축 | `new Set(x).add(y)` 는 재구축이 아니라 '수정한 파생 복사본'이다 | 코퍼스 검증 | 생성 직후 체이닝된 수정은 제외 | `loop-invariant-index.ts` |
Expand Down
17 changes: 15 additions & 2 deletions bin/fixearly.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,9 @@ const wantKit = args.includes("--kit"); // cog × git churn = "먼저 고칠 파
// 옛·새 파서를 비교해 영향 0곳(.js 안 JSX 없음), .js 컴포넌트 저장소는 움직인다(kiss-translator A 83 → B+ 77).
// v15(2026-10-09): O(n²) 가 정렬 비교 함수(sort·toSorted) 안의 선형 탐색도 본다. 보드 75곳 실측: 점수·등급 변화 0,
// 후보만 4곳에서 +2~8(storybook·outline·nx·vscode).
const SCORING_VERSION = "v15";
// v16(2026-10-11): O(n²) 가 for-of 헤더 식(한 번 평가)과 Object.values(Enum) 바깥을 뺀다. 코퍼스 75곳 표본 53개 손검증의
// 탐지기 오류 5건 중 둘. 후보 2,144 → 2,110(12곳) · 실측 점수 변화 elysia 27 → 28 한 곳, 등급 변화 없음.
const SCORING_VERSION = "v16";

// 등급 색 (라이트 기준) — 배지·임베드 공용
const GRADE_COLORS = { S: "#0f7a63", A: "#12915a", B: "#7d8a2c", C: "#c0862e", D: "#cb4436", E: "#8f2f24" };
Expand Down Expand Up @@ -937,6 +939,10 @@ function quadOuterDynamic(text) {
const CONSTish = /^[A-Z][A-Z0-9_]{2,}$/; // ALL_CAPS 식별자
function quadConstOuter(text, moduleConsts) {
if (!text) return false;
// [FP:quad-object-values-enum] `Object.values(IntegrityReport)` 는 enum·상수 객체의 고정 목록이다.
// 루트가 `Object` 라 예전엔 데이터로 봤다 — immich integrity.repository 가 이 모양이었다(2026-10-11).
const ov = /^Object\.(?:values|keys|entries)\(\s*([A-Za-z_$][\w$]*)\s*\)/.exec(text.trim());
if (ov) return /^[A-Z]/.test(ov[1]) || moduleConsts.has(ov[1]);
const root = text.split(/[.[(]/)[0].trim();
if (!root) return false;
if (CONSTish.test(root)) return true;
Expand Down Expand Up @@ -1188,7 +1194,14 @@ function analyzeQuadraticLookups(ts, fileContents) {
ts.isPropertyAccessExpression(node.expression) &&
QUAD_ITERATING_METHODS.has(node.expression.name.getText(sf)) &&
isFnLike(child);
if (!alreadyWalked) walk(child, childInLoop, childLocals, childOuter, childLoop, childFn);
if (alreadyWalked) return;
// [FP:quad-loop-header-once] for-of/for-in 의 반복 대상 식과 for 초기화식은 루프 진입 때 한 번 평가된다.
// elysia `for (const t of types.some(…) ? ALL : types)` 를 루프 안 스캔으로 셌다(2026-10-11).
const once = (node.kind === ts.SyntaxKind.ForOfStatement || node.kind === ts.SyntaxKind.ForInStatement)
? child === node.expression
: node.kind === ts.SyntaxKind.ForStatement && child === node.initializer;
if (once) walk(child, inLoop, locals, outerText, loopNode, childFn);
else walk(child, childInLoop, childLocals, childOuter, childLoop, childFn);
});
};
ts.forEachChild(sf, (n) => walk(n, false, new Set(), null, null, null));
Expand Down
8 changes: 8 additions & 0 deletions bin/selftest.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,14 @@ if (generatedSrc) {
miss: ["byId", "people"],
fromJson: (j) => (j.quality?.quadratic?.candidateList || []).map((c) => c.recv),
},
{
file: "quadratic-gates.ts",
key: "quadratic",
label: "O(n²) 게이트",
hit: ["pending"],
miss: ["kinds", "counts"],
fromJson: (j) => (j.quality?.quadratic?.candidateList || []).map((c) => c.recv),
},
{
file: "regex-in-loop.ts",
key: "regexInLoop",
Expand Down
2 changes: 1 addition & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -602,7 +602,7 @@ <h1 class="en">Rules strangers merged.<br>The lines to fix in yours.</h1>
</div>
<div class="heroart">
<div class="glow"></div>
<div class="loopfig"><!--auto:loop--><img src="loop.jpg" width="900" height="900" alt="" loading="eager"><span class="lc c1"><i class="lpn">1</i><span class="ko">배운다 · 고칠 규칙</span><span class="en">Learn · fix rules</span><b>6</b></span><span class="lc c2"><i class="lpn">2</i><span class="ko">고친다 · PR</span><span class="en">Fix · PRs</span><b>85</b></span><span class="lc c3"><i class="lpn">3</i><span class="ko">판정 · 머지</span><span class="en">Verdict · merged</span><b>37</b></span><span class="lc c4"><i class="lpn">4</i><span class="ko">다진다 · 오탐 가드</span><span class="en">Sharpen · FP guards</span><b>45</b></span><div class="lcen"><svg viewBox="0 0 140 140" aria-hidden="true"><circle r="60" cx="70" cy="70" class="trk"/><circle r="60" cx="70" cy="70" stroke="#2563eb" stroke-dasharray="149.83 376.99" stroke-dashoffset="-0.00" style="--d:0.00s"><title>쓰기만 하는 컬렉션 15</title></circle><circle r="60" cx="70" cy="70" stroke="#0f7a63" stroke-dasharray="119.27 376.99" stroke-dashoffset="-152.83" style="--d:0.12s"><title>O(n²) 조회 12</title></circle><circle r="60" cx="70" cy="70" stroke="#60a5fa" stroke-dasharray="37.76 376.99" stroke-dashoffset="-275.10" style="--d:0.24s"><title>불필요한 순차 I/O 4</title></circle><circle r="60" cx="70" cy="70" stroke="#d97706" stroke-dasharray="27.57 376.99" stroke-dashoffset="-315.86" style="--d:0.36s"><title>버린 반환값 3</title></circle><circle r="60" cx="70" cy="70" stroke="#7c8899" stroke-dasharray="17.38 376.99" stroke-dashoffset="-346.42" style="--d:0.48s"><title>버려진 Promise 2</title></circle><circle r="60" cx="70" cy="70" stroke="#a78bfa" stroke-dasharray="7.19 376.99" stroke-dashoffset="-366.80" style="--d:0.60s"><title>전역 정규식 상태 1</title></circle></svg><div class="lnum"><b>37</b><span class="ko">PR 머지됨</span><span class="en">PRs merged</span><em><span class="ko">규칙 6개가 찾아냄</span><span class="en">found by 6 rules</span></em></div></div><p class="lcap"><span class="lleg"><span class="ko"><span><i style="background:#2563eb"></i>쓰기만 하는 컬렉션 15</span><span><i style="background:#0f7a63"></i>O(n²) 조회 12</span><span><i style="background:#60a5fa"></i>불필요한 순차 I/O 4</span><span><i style="background:#d97706"></i>버린 반환값 3</span><span><i style="background:#7c8899"></i>버려진 Promise 2</span><span><i style="background:#a78bfa"></i>전역 정규식 상태 1</span></span><span class="en"><span><i style="background:#2563eb"></i>Write-only collection 15</span><span><i style="background:#0f7a63"></i>O(n²) lookup 12</span><span><i style="background:#60a5fa"></i>Needless sequential I/O 4</span><span><i style="background:#d97706"></i>Discarded pure call 3</span><span><i style="background:#7c8899"></i>Floating promise 2</span><span><i style="background:#a78bfa"></i>Stateful /g regex 1</span></span></span><span class="ko">남의 저장소에서 머지된 PR 과 남의 코드에서 고칠 규칙을 뽑고, 그 규칙으로 다른 저장소를 고쳐 PR 을 낸다. 머지·거절이 다시 규칙을 다듬는다 — 규칙 6개 중 6개가 머지로 검증 · 저장소 26곳 · 거절 26 · 대기 22. </span><span class="en">Fix rules come from PRs strangers merged and from strangers' code; we apply them to other repos as PRs, and merges and rejections sharpen them again — 6 of 6 rules verified by a merge · 26 projects · closed 26 · open 22. </span><a href="#impact"><span class="ko">전체 기록 →</span><span class="en">Full record →</span></a></p><!--/auto:loop--></div>
<div class="loopfig"><!--auto:loop--><img src="loop.jpg" width="900" height="900" alt="" loading="eager"><span class="lc c1"><i class="lpn">1</i><span class="ko">배운다 · 고칠 규칙</span><span class="en">Learn · fix rules</span><b>6</b></span><span class="lc c2"><i class="lpn">2</i><span class="ko">고친다 · PR</span><span class="en">Fix · PRs</span><b>85</b></span><span class="lc c3"><i class="lpn">3</i><span class="ko">판정 · 머지</span><span class="en">Verdict · merged</span><b>37</b></span><span class="lc c4"><i class="lpn">4</i><span class="ko">다진다 · 오탐 가드</span><span class="en">Sharpen · FP guards</span><b>47</b></span><div class="lcen"><svg viewBox="0 0 140 140" aria-hidden="true"><circle r="60" cx="70" cy="70" class="trk"/><circle r="60" cx="70" cy="70" stroke="#2563eb" stroke-dasharray="149.83 376.99" stroke-dashoffset="-0.00" style="--d:0.00s"><title>쓰기만 하는 컬렉션 15</title></circle><circle r="60" cx="70" cy="70" stroke="#0f7a63" stroke-dasharray="119.27 376.99" stroke-dashoffset="-152.83" style="--d:0.12s"><title>O(n²) 조회 12</title></circle><circle r="60" cx="70" cy="70" stroke="#60a5fa" stroke-dasharray="37.76 376.99" stroke-dashoffset="-275.10" style="--d:0.24s"><title>불필요한 순차 I/O 4</title></circle><circle r="60" cx="70" cy="70" stroke="#d97706" stroke-dasharray="27.57 376.99" stroke-dashoffset="-315.86" style="--d:0.36s"><title>버린 반환값 3</title></circle><circle r="60" cx="70" cy="70" stroke="#7c8899" stroke-dasharray="17.38 376.99" stroke-dashoffset="-346.42" style="--d:0.48s"><title>버려진 Promise 2</title></circle><circle r="60" cx="70" cy="70" stroke="#a78bfa" stroke-dasharray="7.19 376.99" stroke-dashoffset="-366.80" style="--d:0.60s"><title>전역 정규식 상태 1</title></circle></svg><div class="lnum"><b>37</b><span class="ko">PR 머지됨</span><span class="en">PRs merged</span><em><span class="ko">규칙 6개가 찾아냄</span><span class="en">found by 6 rules</span></em></div></div><p class="lcap"><span class="lleg"><span class="ko"><span><i style="background:#2563eb"></i>쓰기만 하는 컬렉션 15</span><span><i style="background:#0f7a63"></i>O(n²) 조회 12</span><span><i style="background:#60a5fa"></i>불필요한 순차 I/O 4</span><span><i style="background:#d97706"></i>버린 반환값 3</span><span><i style="background:#7c8899"></i>버려진 Promise 2</span><span><i style="background:#a78bfa"></i>전역 정규식 상태 1</span></span><span class="en"><span><i style="background:#2563eb"></i>Write-only collection 15</span><span><i style="background:#0f7a63"></i>O(n²) lookup 12</span><span><i style="background:#60a5fa"></i>Needless sequential I/O 4</span><span><i style="background:#d97706"></i>Discarded pure call 3</span><span><i style="background:#7c8899"></i>Floating promise 2</span><span><i style="background:#a78bfa"></i>Stateful /g regex 1</span></span></span><span class="ko">남의 저장소에서 머지된 PR 과 남의 코드에서 고칠 규칙을 뽑고, 그 규칙으로 다른 저장소를 고쳐 PR 을 낸다. 머지·거절이 다시 규칙을 다듬는다 — 규칙 6개 중 6개가 머지로 검증 · 저장소 26곳 · 거절 26 · 대기 22. </span><span class="en">Fix rules come from PRs strangers merged and from strangers' code; we apply them to other repos as PRs, and merges and rejections sharpen them again — 6 of 6 rules verified by a merge · 26 projects · closed 26 · open 22. </span><a href="#impact"><span class="ko">전체 기록 →</span><span class="en">Full record →</span></a></p><!--/auto:loop--></div>
</div>
</div>
</header>
Expand Down
28 changes: 28 additions & 0 deletions tools/fixtures/quadratic-gates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// O(n²) 게이트 픽스처 — 루프처럼 보이지만 한 번만 도는 자리와 고정 목록 바깥.
import { Report } from "./report";

interface Row { id: string; type: string; count: number }

// pending: 바깥(rows)도 안쪽(pending)도 데이터다 — 잡아야 한다.
export function matchPending(rows: Row[], pending: Row[]): Row[] {
const out: Row[] = [];
for (const row of rows) {
const hit = pending.find((p) => p.id === row.id);
if (hit) out.push(hit);
}
return out;
}

// kinds: for-of 의 반복 대상 식은 루프 진입 때 한 번 평가된다(elysia). [FP:quad-loop-header-once]
export function expand(kinds: string[], all: string[]): string[] {
const out: string[] = [];
for (const k of kinds.some((x) => x === "all") ? all : kinds) out.push(k);
return out;
}

// counts: 바깥이 enum 의 고정 목록이다(immich). [FP:quad-object-values-enum]
export function tally(counts: Row[]): Record<string, number> {
return Object.fromEntries(
Object.values(Report).map((t) => [t, counts.find((c) => c.type === t)?.count || 0]),
);
}
Loading