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
34 changes: 34 additions & 0 deletions scripts/__tests__/check-skill-examples.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1023,6 +1023,40 @@ describe('the bare-`any` assertion', () => {
expect(findBareAny(code)).toEqual([]);
});

// ── The two shapes whose LABEL was the defect (objectui#7653, ported from
// objectstack#14910). BOTH were already flagged before the fix, so a pin
// asserting only "it is a finding" passes on the broken code and proves
// nothing. Both legs therefore assert the LABEL STRING, because the label
// is the bug — and it is half a finding's baseline row key, so a wrong one
// is a finding that cannot be declared the day such a site is marked.
it('labels an `any` index signature `index signature`, not `return type`', () => {
// `ts.isFunctionLike` is true for every SignatureDeclaration kind and an
// IndexSignatureDeclaration IS one, so before the fix this read
// `return type` — a position the `any` does not occupy. Flagging is right
// either way (an `any` index signature erases checking on every keyed
// access), so the fix is a LABEL and the arm sits BEFORE the function-like
// fallback. This pin fails on the broken code because it asserts the label.
const hits = findBareAny('interface Bag {\n [key: string]: any;\n}\n') as { where: string }[];
expect(hits.map((h) => h.where)).toEqual(['index signature']);
});

it('keeps a return `any` on a function type inside a type argument as `return type`', () => {
// The boundary is the DIRECT parent (`parent.type === node`), never
// ancestry: in `Array<() => any>` the `any`'s parent is the FunctionTypeNode
// whose return slot it fills, not the TypeReference above it. The three
// shapes the header calls "nested" all have that predicate FALSE; this one
// has it TRUE, so it is a finding and stays one.
//
// ⛔ This pin is the header's rule made executable, and its value is that it
// can FAIL: the day someone reads "nested `any` is deliberately not flagged"
// as ancestry and narrows the function-like arm, this goes red instead of
// quietly handing authors the one-token evasion the rule already refuses for
// parameters — wrap the offending function type in a type argument and the
// gate would go green over an unchanged defect.
const hits = findBareAny('const fns: Array<() => any> = [];\nvoid fns;\n') as { where: string }[];
expect(hits.map((h) => h.where)).toEqual(['return type']);
});

it('parses as TSX, so a JSX example is not mis-read as a type assertion', () => {
// `compileSnippets` parses every block as TSX regardless of the fence
// label. A guard walking a different tree would be reporting about a
Expand Down
82 changes: 80 additions & 2 deletions scripts/check-skill-examples.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,25 @@
* `satisfies any` / angle-bracket assertion. `any` NESTED inside a larger type
* (`Record<string, any>`, `any[]`, `Promise<any>`) is deliberately NOT flagged.
* That boundary is the zero-false-positive line, and holding it is what keeps a
* red meaning broken. Casts and locals are in scope and not for symmetry: a
* red meaning broken.
*
* ⭐ "NESTED" IS THE DIRECT PARENT AND NOTHING ELSE — read the predicate, never
* the ancestry (objectui#7653, ported from objectstack#14910). The operative
* test is `parent.type === node`: all three names above have it FALSE (the
* `any`'s parent is the TypeReference or ArrayType that contains it), while a
* function type's return `any` standing inside a type argument has it TRUE and
* IS a finding, labelled `return type` — in `Array<() => any>` the `any`'s
* parent is the FunctionTypeNode whose return slot it fills, not the
* TypeReference above it. So "nested" describes an `any` that is a COMPONENT of
* a composite type, never an `any` that merely has a type argument among its
* ancestors. Narrowing the function-like arm to an ancestry reading would hand
* an author going red the one-token evasion this rule already refuses for
* parameters: wrap the offending function type in a type argument and the gate
* goes green over an unchanged defect. The self-test's `red-positions` fixture
* and the suite's boundary pin both assert that LABEL, so the narrowing reds
* rather than passing quietly.
*
* Casts and locals are in scope and not for symmetry: a
* parameter-only rule is defeated by exactly the edit an author reaches for
* when it goes red — move the `any` one line down (`const c: any = ctx`) or
* into the access (`(ctx as any).x`) — leaving the gate green over an unchanged
Expand Down Expand Up @@ -1020,6 +1038,19 @@ export function classifyShadowedTypes(hits, markedHits, declared = KNOWN_SHADOWE
* finding. That boundary is this guard's zero-false-positive line; widening it
* is a different question with a different, much larger baseline.
*
* The boundary is the DIRECT parent and nothing else — see the SCOPE paragraph
* at the top of this file for why `Array<() => any>` IS a finding, labelled
* `return type`, and why reading "nested" as ancestry would open an evasion.
*
* ORDER MATTERS in one place. `ts.isFunctionLike` is true for every
* SignatureDeclaration kind, IndexSignatureDeclaration included, so a bare
* `[k: string]: any` would fall into the `return type` arm and be reported at a
* position it does not occupy (objectui#7653). Flagging it is right — an `any`
* index signature erases checking on every keyed access — so the fix is a
* label, not an exclusion, and the arm has to come BEFORE the function-like
* fallback. The label is half of a finding's row key, so a wrong one is a
* finding that cannot be declared or baselined the day such a site is marked.
*
* Returns the human-readable position (which is also half the baseline key), or
* `null` when this `any` is not in a checking-erasing position.
*/
Expand All @@ -1042,7 +1073,12 @@ export function describeAnyPosition(node) {
// stays whole if that ever changes; see the header's divergence note.
if (ts.isTypeAssertionExpression(parent) && parent.type === node)
return 'angle-bracket `any` assertion';
// Return annotations: functions, methods, arrows, getters, signatures.
// BEFORE the function-like fallback: an IndexSignatureDeclaration IS a
// SignatureDeclaration, so `isFunctionLike` claims it and its `any` would be
// reported as a `return type` it does not have.
if (ts.isIndexSignatureDeclaration(parent) && parent.type === node) return 'index signature';
// Return annotations: functions, methods, arrows, getters, signatures —
// including a FunctionTypeNode standing inside a type argument.
if (ts.isFunctionLike(parent) && parent.type === node) return 'return type';
return null;
}
Expand Down Expand Up @@ -2176,6 +2212,48 @@ export function selfTest() {
);
}

// ── POSITIONS (objectui#7653): the two shapes whose LABEL was the defect. ──
// BOTH were already flagged before this fix, so a leg asserting only "it is
// a finding" passes on the broken code and pins nothing. Each leg therefore
// asserts the LABEL STRING, because the label is half a finding's row key.
//
// 1. An `any` index signature was reported as `return type`:
// `ts.isFunctionLike` is true for every SignatureDeclaration kind, and an
// IndexSignatureDeclaration is one. Flagging stays; the label is now
// `index signature`.
// 2. A function type's return `any` standing inside a type argument is a
// `return type` finding and STAYS one — the boundary is the DIRECT
// parent, so `Array<() => any>` is the function type's return slot, not
// a "nested" `any`. This leg is the header's rule made executable: the
// day someone reads "nested `any` is deliberately not flagged" as
// ancestry and narrows the arm, it goes red here rather than silently
// handing authors a one-token evasion.
//
// Driven as ONE snippet so the ORDER of the two arms is pinned too: the
// index-signature arm must sit BEFORE the function-like fallback.
const positionsCode = [
'interface Bag {', // 1
' [key: string]: any;', // 2 ← index signature, NOT a return type
'}', // 3
'const fns: Array<() => any> = [];', // 4 ← return type, inside a type argument
'void fns;', // 5
'',
].join('\n');
{
const got = findBareAny(positionsCode)
.map((h) => `${h.line}:${h.where}`)
.sort();
const want = ['2:index signature', '4:return type'].sort();
t(
'an `any` index signature is labelled `index signature`, and a return `any` on a function type inside a type argument stays `return type`',
JSON.stringify(got) === JSON.stringify(want),
`got ${JSON.stringify(got)}, want ${JSON.stringify(want)} — an \`any\` index signature must be labelled ` +
`"index signature" (\`isFunctionLike\` matches an IndexSignatureDeclaration, so it used to read ` +
`"return type"), and a function type's return \`any\` inside a type argument must STAY a "return type" ` +
`finding (the boundary is the DIRECT parent, not ancestry)`,
);
}

const anyNegative = [
['Record<string, any>', 'export const a: Record<string, any> = {};'],
['any[]', 'export const b: any[] = [];'],
Expand Down
Loading