diff --git a/graph/test/typescript/cases/32-decorator-application/src/svc.ts b/graph/test/typescript/cases/32-decorator-application/src/svc.ts index 36d8c88e4..37601b24c 100644 --- a/graph/test/typescript/cases/32-decorator-application/src/svc.ts +++ b/graph/test/typescript/cases/32-decorator-application/src/svc.ts @@ -1,12 +1,10 @@ -// TWO FORMS OF THE SAME CONSTRUCT, and the parser treats them differently. +// TWO FORMS OF THE SAME CONSTRUCT, and both are calls. // // A decorator is a function the runtime invokes with (target, key, descriptor), and the -// compiler resolves the application in both forms. The parser emits a ts_call_site for -// the FACTORY form only; the bare form gets a ts_decorator row with kind = MARKER and no -// call site at all, so the application is absent from the IR and cannot be scored. See -// parser#82 — re-priced there on 15 sites inside one repository, which is not what it -// costs on a codebase that decorates most public methods (191 sites, 89% of one -// project's whole conservation loss). +// compiler resolves the application in both forms. The FACTORY form is a CallExpression +// and always had a call site; the BARE form is only a name, and until #233 it had no call +// site at all, so the application was absent from the IR. On a codebase that decorates +// most public methods that was 191 sites, 89% of one project's whole conservation loss. export function guarded(_t: unknown, _k: string, d: PropertyDescriptor): PropertyDescriptor { return d; } @@ -15,16 +13,43 @@ export function timed(): MethodDecorator { return (_t, _k, d) => d; } +export function sealed(ctor: Function): void { + Object.seal(ctor); +} + +export class Guards { + static disposed(_t: unknown, _k: string, d: PropertyDescriptor): PropertyDescriptor { + return d; + } +} + +@sealed export class Svc { - // BARE — the gap. No call site of any kind is emitted for this. + // BARE, a name: the application is a call to `guarded`. @guarded a(): string { return "a"; } - // THE CONTROL: the factory form, which does emit a call site for the factory call. + // THE CONTROL: the factory form. ONE call site, for `timed()`; the name inside it is + // that call's callee and must not become a second call. @timed() b(): string { return "b"; } + + // BARE, a member of a class: `Guards.disposed` is called, not read. + @Guards.disposed + c(): string { + return "c"; + } + + // BARE, parenthesised: the parentheses are punctuation, still one call. + @(guarded) + d(): string { + return "d"; + } } + +// THE CONTROL: the same name outside a decorator is a reference, not a call. +export const alias = guarded; diff --git a/graph/test/typescript/expected/32-decorator-application.edges b/graph/test/typescript/expected/32-decorator-application.edges index a536ed7e7..b1a896cc9 100644 --- a/graph/test/typescript/expected/32-decorator-application.edges +++ b/graph/test/typescript/expected/32-decorator-application.edges @@ -1 +1,6 @@ -known_edge DECORATOR_CALL Svc#b() @L26 -> svc#timed() +ambiguous_unknown METHOD_CALL svc#sealed(Function) @L17 -> - +known_edge DECORATOR_CALL Svc#a() @L29 -> svc#guarded(unknown,string,PropertyDescriptor) +known_edge DECORATOR_CALL Svc#b() @L36 -> svc#timed() +known_edge DECORATOR_CALL Svc#c() @L42 -> Guards#static disposed(unknown,string,PropertyDescriptor) +known_edge DECORATOR_CALL Svc#d() @L48 -> svc#guarded(unknown,string,PropertyDescriptor) +known_edge DECORATOR_CALL svc#() @L26 -> svc#sealed(Function) diff --git a/graph/test/typescript/expected/32-decorator-application.entries b/graph/test/typescript/expected/32-decorator-application.entries new file mode 100644 index 000000000..6e1c6d4f1 --- /dev/null +++ b/graph/test/typescript/expected/32-decorator-application.entries @@ -0,0 +1,5 @@ +── entry_point (4) ── + exported_from_entry_module svc#guarded svc.ts:8 + exported_from_entry_module svc#sealed svc.ts:16 + exported_from_entry_module svc#timed svc.ts:12 + unimported_module svc# svc.ts:1 diff --git a/graph/test/typescript/expected/32-decorator-application.known-missing b/graph/test/typescript/expected/32-decorator-application.known-missing deleted file mode 100644 index 037af29f1..000000000 --- a/graph/test/typescript/expected/32-decorator-application.known-missing +++ /dev/null @@ -1,19 +0,0 @@ -# Accepted gap for 32-decorator-application. A NEW missing edge fails the suite; a line -# here that STARTS working also fails, so the debt cannot rot. -# -# PARSER-BLOCKED, and the engine cannot close it. A bare `@guarded` gets a ts_decorator -# row with kind = MARKER and NO ts_call_site, so the application is absent from the IR -# entirely. The engine has no site to attach an edge to, and score.py joins the oracle to -# IR call sites by position — so the compiler's row becomes CONSERVATION LOSS and leaves -# the denominator rather than counting as a miss. Nothing in the rules can recover that; -# it needs a call site emitted. parser#82, and #233 for the measurement. -# -# The FACTORY form beside it is the control and resolves: `@timed()` does emit a call -# site for the factory call, which is what makes this attributable to the bare form and -# not to decorators in general. -# -# This case is also the first in the suite to use a decorator at all. Until #233 the case -# oracle's node list omitted `ts.isDecorator` while the project oracle emitted -# DECORATOR_CALL, so the per-case suite was green on decorators whatever the engine did. -# Both sides now say the same thing, which is why this line can exist. -Svc#a() -> svc#guarded(unknown,string,PropertyDescriptor) diff --git a/graph/test/typescript/expected/32-decorator-application.oracle b/graph/test/typescript/expected/32-decorator-application.oracle index a955086d4..6c4bb5cbb 100644 --- a/graph/test/typescript/expected/32-decorator-application.oracle +++ b/graph/test/typescript/expected/32-decorator-application.oracle @@ -1,2 +1 @@ -oracle=2 engine=1 agree=1 missing=1 (known 1, NEW 0) extra=0 - known Svc#a() -> svc#guarded(unknown,string,PropertyDescriptor) +oracle=5 engine=5 agree=5 missing=0 (known 0, NEW 0) extra=0 diff --git a/graph/test/typescript/expected/32-decorator-application.type-use b/graph/test/typescript/expected/32-decorator-application.type-use index 1f8549a0d..33144774e 100644 --- a/graph/test/typescript/expected/32-decorator-application.type-use +++ b/graph/test/typescript/expected/32-decorator-application.type-use @@ -1,3 +1,5 @@ ambiguous_unknown DECORATOR_TYPE 0 Svc [DECORATOR] -> - +ambiguous_unknown METHOD_PARAM 0 Guards [METHOD_PARAM] -> - ambiguous_unknown METHOD_PARAM 0 svc [METHOD_PARAM] -> - +ambiguous_unknown METHOD_RETURN 0 Guards [METHOD] -> - ambiguous_unknown METHOD_RETURN 0 svc [METHOD] -> - diff --git a/graph/test/typescript/expected/32-decorator-application.types-oracle b/graph/test/typescript/expected/32-decorator-application.types-oracle index f06517c0b..4a9e4f17a 100644 --- a/graph/test/typescript/expected/32-decorator-application.types-oracle +++ b/graph/test/typescript/expected/32-decorator-application.types-oracle @@ -1,7 +1,7 @@ 32-decorator-application [types] precision 0.0000 (0 correct, 0 wrong) recall 0.0000 (0 of 0 the compiler resolved) - sites 5 resolved 0 (0.0%) - tiers ambiguous_unknown=5 - contexts DECORATOR_TYPE=2 METHOD_PARAM=1 METHOD_RETURN=2 - not scored: 5 rows whose target is not a client declaration + sites 11 resolved 0 (0.0%) + tiers ambiguous_unknown=11 + contexts DECORATOR_TYPE=5 METHOD_PARAM=3 METHOD_RETURN=3 + not scored: 11 rows whose target is not a client declaration diff --git a/graph/test/typescript/ground-truth/tsc-oracle.mjs b/graph/test/typescript/ground-truth/tsc-oracle.mjs index a30200c0b..68fd01f35 100644 --- a/graph/test/typescript/ground-truth/tsc-oracle.mjs +++ b/graph/test/typescript/ground-truth/tsc-oracle.mjs @@ -356,7 +356,12 @@ for (const sf of program.getSourceFiles()) { // So the loss was a convention disagreement, not a missing site, and it is fixed // on this side because the parser's choice is the more useful one: the identifier // is what a reader looks for, and `@` is not part of the callee. - const posNode = ts.isDecorator(node) ? node.expression : node; + // `@(guarded)` is positioned at `guarded`, as the parser does: the parentheses + // are punctuation, not the callee. + let posNode = ts.isDecorator(node) ? node.expression : node; + while (ts.isDecorator(node) && ts.isParenthesizedExpression(posNode)) { + posNode = posNode.expression; + } const [line, col] = pos(sf, posNode.getStart(sf)); // The END position is part of the key, not decoration. A chained call // `a.b().c()` and its inner `a.b()` START at the same character, so diff --git a/graph/typescript/engine/expression-resolution/callee-resolution.dl b/graph/typescript/engine/expression-resolution/callee-resolution.dl index 20aed1071..c674c6ad8 100644 --- a/graph/typescript/engine/expression-resolution/callee-resolution.dl +++ b/graph/typescript/engine/expression-resolution/callee-resolution.dl @@ -39,6 +39,18 @@ call_callee_expr(cs, ce, ne) :- call_site("client", _, _, _, _, ce, cs), expr_child("client", ce, "METHOD_NAME", _, ne). +// ── A BARE DECORATOR IS ITS OWN CALLEE ────────────────────────────────────── +// `@guarded` has no CALL_EXPRESSION around it: the runtime invokes the named function +// with (target, key, descriptor), so the parser's call site points at the NAME itself, +// and that name is also the callee position. `@timed()` keeps its ordinary shape — the +// call is the site and `timed` its METHOD_NAME child. On a codebase that decorates most +// public methods this was most of the compiler's sites missing from the graph (#233). +bare_decorator_call(ce) :- call_site("client", "DECORATOR_CALL", _, _, _, ce, _), + expr_kind("client", k, _, ce), + k != "CALL_EXPRESSION". +call_callee_expr(cs, ce, ce) :- call_site("client", "DECORATOR_CALL", _, _, _, ce, cs), + bare_decorator_call(ce). + // A receiver type must be searchable, so every type a receiver actually takes is // seeded into the member-lookup scope. Bounded to types used as receivers, exactly // as Java bounds `lookup_type`; without it a static-qualifier receiver reached only diff --git a/graph/typescript/engine/expression-resolution/expr-type.dl b/graph/typescript/engine/expression-resolution/expr-type.dl index f7251ef11..30c4bcc97 100644 --- a/graph/typescript/engine/expression-resolution/expr-type.dl +++ b/graph/typescript/engine/expression-resolution/expr-type.dl @@ -614,19 +614,28 @@ expr_exact_type(e, t) :- expr_kind("client", "NEW_EXPRESSION", _, e), // ── a CALL result — `a().b()` ────────────────────────────────────────────── // The chained-receiver case, and the one that closes the recursion. The SOUND // candidate set is used, never the disambiguated one; see the header. +// A bare decorator's site IS its name (callee-resolution.dl), and nothing reads what the +// application returns, so the result is not typed onto the name: that would give `guarded` +// the type of the descriptor it returns, and a callable result would feed back as another +// candidate of the same site. expr_type(e, prov, t) :- expr_call_candidate(e, callee), + !bare_decorator_call(e), method_return_type(callee, prov, t). expr_shape(e, s) :- expr_call_candidate(e, callee), + !bare_decorator_call(e), method_return_shape(callee, s). expr_call_returns_awaited(e, prov, t) :- expr_call_candidate(e, callee), + !bare_decorator_call(e), method_return_awaited(callee, prov, t). expr_element_type(e, prov, t) :- expr_call_candidate(e, callee), + !bare_decorator_call(e), method_return_element(callee, prov, t). // A call whose callee returns `this` — the fluent-builder idiom — keeps the // receiver's type rather than taking the declaring type's. expr_type(e, prov, t) :- ts_call_site(_, _, _, recv, _, e, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _), recv != "", expr_call_candidate(e, callee), + !bare_decorator_call(e), // THE CALLEE'S PROVENANCE IS A VARIABLE, NOT "client". A fluent API declared in a // dependency returns `this` from a LIBRARY declaration, so pinning this to the // client meant the chain stopped dead at the first hop into one: the call itself diff --git a/graph/typescript/engine/expression-resolution/overload.dl b/graph/typescript/engine/expression-resolution/overload.dl index 486fd5f08..948d5b68e 100644 --- a/graph/typescript/engine/expression-resolution/overload.dl +++ b/graph/typescript/engine/expression-resolution/overload.dl @@ -87,6 +87,9 @@ call_expr_argc(ce, n) :- call_site("client", "TAGGED_TEMPLATE_CALL", _, _, _, ce call_has_spread(ce) :- call_site("client", _, _, _, _, ce, cs), call_shape("client", _, si, _, cs), si != "". +// A BARE DECORATOR WRITES NO ARGUMENTS and receives two or three, by decorator system and +// target, so its count is unknown in the same sense a spread's is: arity stands down. +call_has_spread(ce) :- bare_decorator_call(ce). // ── arity_match(CallExpr, Callee) ─────────────────────────────────────────── // Guarded by the candidate set, so the numeric comparison is never a cross product diff --git a/graph/typescript/souffle/decls_all.dl b/graph/typescript/souffle/decls_all.dl index 9102e4c5c..f48cb98bb 100644 --- a/graph/typescript/souffle/decls_all.dl +++ b/graph/typescript/souffle/decls_all.dl @@ -23,6 +23,7 @@ .decl body_presence_is_ambient(c0:symbol) .decl body_presence_is_overload(c0:symbol) .decl body_presence_is_signature(c0:symbol) +.decl bare_decorator_call(c0:symbol) .decl call_callee_expr(c0:symbol,c1:symbol,c2:symbol) .decl call_caller(c0:symbol,c1:symbol,c2:symbol,c3:symbol,c4:symbol) .decl call_caller_method(c0:symbol,c1:symbol) diff --git a/parser/src/parsers/typescript/extractors/ts-expression-extractor.ts b/parser/src/parsers/typescript/extractors/ts-expression-extractor.ts index 330e71177..c5ebcb361 100644 --- a/parser/src/parsers/typescript/extractors/ts-expression-extractor.ts +++ b/parser/src/parsers/typescript/extractors/ts-expression-extractor.ts @@ -178,7 +178,7 @@ export class TsExpressionExtractor { this.emitted.push({ node: item.node, row }); if (kind === TsExpressionKind.CALL_EXPRESSION || kind === TsExpressionKind.NEW_EXPRESSION || kind === TsExpressionKind.TAGGED_TEMPLATE || kind === TsExpressionKind.JSX_ELEMENT - || kind === TsExpressionKind.JSX_SELF_CLOSING) { + || kind === TsExpressionKind.JSX_SELF_CLOSING || isBareDecorator(item.node)) { this.callNodes.push({ node: item.node, owner: item.owner }); } @@ -856,6 +856,11 @@ function operatorOf(node: ts.Node, sourceFile: ts.SourceFile): string { /** The callee of a call-shaped node, parentheses unwrapped. */ export function calleeOf(node: ts.Node): ts.Node | undefined { + // `@guarded` has no call node around it: the name IS the callee, and the + // runtime supplies the arguments. + if (isBareDecorator(node)) { + return node; + } if (ts.isCallExpression(node) || ts.isNewExpression(node)) { return unwrapParentheses(node.expression); } @@ -953,6 +958,22 @@ function isDecoratorCall(node: ts.Node): boolean { return current !== undefined && ts.isDecorator(current); } +/** + * Whether this node is a decorator applied WITHOUT a call: `@guarded`, + * `@guards.disposed`, `@registry["x"]`. + * + * The runtime invokes the named function with (target, key, descriptor), so the + * application is a call exactly as `@timed()` is, and the compiler resolves it. + * Without a call site of its own it was absent from the IR, which on a codebase + * that decorates most public methods is most of what goes missing (#233). The + * factory form is excluded: its CallExpression is already the call site, and + * the name inside it is that call's callee, not a second call. + */ +export function isBareDecorator(node: ts.Node): boolean { + return (ts.isIdentifier(node) || ts.isPropertyAccessExpression(node) + || ts.isElementAccessExpression(node)) && isDecoratorCall(node); +} + function callKindOf(node: ts.Node, callee: ts.Node | undefined): TsCallKind { if (ts.isNewExpression(node)) { return TsCallKind.CONSTRUCTOR_CALL; diff --git a/parser/src/parsers/typescript/extractors/ts-ir-completeness.ts b/parser/src/parsers/typescript/extractors/ts-ir-completeness.ts index 189051d2f..de5be2f9d 100644 --- a/parser/src/parsers/typescript/extractors/ts-ir-completeness.ts +++ b/parser/src/parsers/typescript/extractors/ts-ir-completeness.ts @@ -840,6 +840,11 @@ function calleeExpressionOf( if (!callRow) { return undefined; } + // A bare `@guarded` is its own callee: the site is the name, with no call + // expression around it (#233). + if (callSite.callKind === 'DECORATOR_CALL' && callRow.kind !== 'CALL_EXPRESSION') { + return callRow; + } return (childrenByParent.get(callRow.getHash()) ?? []) .find((child) => child.edgeRole === 'METHOD_NAME'); } diff --git a/parser/src/parsers/typescript/extractors/ts-resolution-linker.ts b/parser/src/parsers/typescript/extractors/ts-resolution-linker.ts index e7bd514dc..7272f574f 100644 --- a/parser/src/parsers/typescript/extractors/ts-resolution-linker.ts +++ b/parser/src/parsers/typescript/extractors/ts-resolution-linker.ts @@ -25,6 +25,7 @@ import { } from '@/parsers/typescript/extractors/ts-binder'; import { calleeOf, + isBareDecorator, unwrapParentheses, } from '@/parsers/typescript/extractors/ts-expression-extractor'; @@ -1226,6 +1227,13 @@ function callArgumentCount(node: ts.Node): number { if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node)) { return 1; } + // A bare `@guarded` writes no arguments; the runtime passes two or three + // depending on the decorator system and the target, which this pass does not + // know. No count admits any parameterised signature, so a sole candidate is + // still taken and an overload set is left unchosen rather than guessed. + if (isBareDecorator(node)) { + return -1; + } return 0; } diff --git a/parser/src/test/typescript-tests.ts b/parser/src/test/typescript-tests.ts index 1cb810987..fd295003d 100644 --- a/parser/src/test/typescript-tests.ts +++ b/parser/src/test/typescript-tests.ts @@ -812,6 +812,7 @@ function tscAdjudicatedResolution(): number { const corpora = extractedCorpora(); const failures: string[] = []; let expectedCalls = 0, emittedCalls = 0, projectTargets = 0, agreed = 0, unfilled = 0; + let bareDecorators = 0; const byReceiver = new Map(); for (const c of index.corpora) { @@ -866,8 +867,13 @@ function tscAdjudicatedResolution(): number { } } for (const [where, leftover] of emitted) { - if (leftover.length > 0) { - failures.push(`${c.slug}: parser emits ${leftover.length} extra call site(s) at ${where}`); + // A BARE DECORATOR is a call the compiler resolves (#233), but this oracle + // enumerates call EXPRESSIONS and `@guarded` is only a name, so it has no row + // to match. Counted, not failed: case 32 scores them against the compiler. + const extra = leftover.filter((r) => r.callKind !== 'DECORATOR_CALL'); + bareDecorators += leftover.length - extra.length; + if (extra.length > 0) { + failures.push(`${c.slug}: parser emits ${extra.length} extra call site(s) at ${where}`); } } } @@ -878,7 +884,7 @@ function tscAdjudicatedResolution(): number { .join(' '); console.log(` ${emittedCalls} call site(s) emitted for ${expectedCalls} tsc sees; ` + `${projectTargets} project targets: ${agreed} agree, ${unfilled} left to the engine, ` + - `${failures.length} disagree`); + `${failures.length} disagree; ${bareDecorators} bare decorator(s) this oracle does not enumerate`); // PROVENANCE, not a score. The unfilled column is where the ENGINE resolves, // which is the design; see `IR completeness` for the number that matters. console.log(` provenance — same-file links by receiver shape: ${shapes}`); @@ -1134,11 +1140,17 @@ function factBaseInvariants(): number { } } - // 8. One call site per CALL / NEW / TAGGED_TEMPLATE expression, exactly. - const callShaped = (all.get('all-typescript-expressions.csv') ?? []) + // 8. One call site per CALL / NEW / TAGGED_TEMPLATE expression, exactly, and + // one per bare decorator (`@guarded`), whose site is the name itself (#233). + const expressions = all.get('all-typescript-expressions.csv') ?? []; + const callSiteRows = all.get('all-typescript-call-sites.csv') ?? []; + const kindOfExpression = new Map(expressions.map((r) => [r.tsExpressionUniqueHash ?? '', r.kind])); + const bareDecorators = callSiteRows.filter((r) => r.callKind === 'DECORATOR_CALL' + && kindOfExpression.get(r.tsExpressionLinkHash ?? '') !== 'CALL_EXPRESSION').length; + const callShaped = expressions .filter((r) => r.kind === 'CALL_EXPRESSION' || r.kind === 'NEW_EXPRESSION' - || r.kind === 'TAGGED_TEMPLATE').length; - const callSites = (all.get('all-typescript-call-sites.csv') ?? []).length; + || r.kind === 'TAGGED_TEMPLATE').length + bareDecorators; + const callSites = callSiteRows.length; if (callShaped !== callSites) { failures.push(`${slug}: ${callShaped} call-shaped expression(s) but ${callSites} ` + 'ts_call_site row(s) — the 1:1 chain is broken'); @@ -2807,6 +2819,74 @@ async function decoratorDescendedOnce(): Promise { } } +/** + * A bare decorator is a call site (#233). + * + * `@guarded` is invoked by the runtime exactly as `@timed()` is, and the compiler + * resolves both, but only the factory form had a `ts_call_site`: the bare one was + * absent from the IR. Each bare application is one DECORATOR_CALL positioned at its + * name, where the compiler positions it. THE CONTROLS: the factory form stays ONE + * site (its callee is not a second call), and the same name outside a decorator is + * no call at all. + */ +async function bareDecoratorIsACallSite(): Promise { + if (!parserPresent()) { + return pendingCheck('a bare decorator is a call site', + 'no extractor yet. `@guarded` is a call the runtime makes'); + } + const { outputDir, cleanup } = await analyseInline('ts-baredec-', { + 'a.ts': [ + 'export function guarded(_t: unknown, _k: string, d: PropertyDescriptor) { return d; }', // 1 + 'export function timed(): MethodDecorator { return (_t, _k, d) => d; }', // 2 + 'export class Guards {', // 3 + ' static disposed(_t: unknown, _k: string, d: PropertyDescriptor) { return d; }', // 4 + '}', // 5 + 'export class Svc {', // 6 + ' @guarded a() {}', // 7 + ' @timed() b() {}', // 8 + ' @Guards.disposed c() {}', // 9 + ' @(guarded) d() {}', // 10 + '}', // 11 + 'export const alias = guarded;', // 12 + ].join('\n'), + }, { experimentalDecorators: true }); + try { + const sites = relation(outputDir, 'all-typescript-call-sites.csv'); + const failures: string[] = []; + const expected: [string, string, string][] = [ + ['7', '4', 'guarded'], ['8', '4', 'timed'], ['9', '4', 'disposed'], ['10', '5', 'guarded'], + ]; + for (const [line, column, name] of expected) { + const at = sites.filter((r) => r['startLine'] === line); + if (at.length !== 1) { + failures.push(`${at.length} call site(s) on line ${line}, expected 1`); + continue; + } + const row = at[0]!; + if (row['callKind'] !== 'DECORATOR_CALL' || row['calleeName'] !== name + || row['startColumn'] !== column) { + failures.push(`line ${line}: ${row['callKind']} ${row['calleeName']} @col ` + + `${row['startColumn']}, expected DECORATOR_CALL ${name} @col ${column}`); + } + } + // A local, single-signature decorator is resolved by the parser like any call. + const bare = sites.find((r) => r['startLine'] === '7'); + if (bare && (bare['resolvedSignatureLinkHash'] ?? '') === '') { + failures.push('`@guarded` names a local function and was left unresolved'); + } + const alias = sites.filter((r) => r['startLine'] === '12'); + if (alias.length !== 0) { + failures.push(`${alias.length} call site(s) for \`alias = guarded\`, a reference, not a call`); + } + console.log(` ${sites.length} call site(s); ` + + `${sites.filter((r) => r['callKind'] === 'DECORATOR_CALL').length} DECORATOR_CALL`); + for (const f of failures.slice(0, 6)) { console.log(` ${f}`); } + return failures.length ? 1 : 0; + } finally { + cleanup(); + } +} + /** * `.d.mts` and `.d.cts` are declaration files, and their stem is not `index.d` * (#89). @@ -4661,6 +4741,7 @@ const CHECKS: Check[] = [ { name: 'column order is append-only', proves: 'a column is never inserted mid-table, because Souffle binds by position and misbinds silently', run: columnOrderIsAppendOnly }, { name: 'governing tsconfig is per file', proves: 'a file reachable from two programs mints ONE ts_module row, so no key carries contradictory moduleResolutionMode', run: governingConfigIsPerFile }, { name: 'a decorator is descended once', proves: 'a callable inside a decorator argument is one ts_method, not two colliding on one key as a false overload set', run: decoratorDescendedOnce }, + { name: 'a bare decorator is a call site', proves: '`@guarded` is one DECORATOR_CALL at its name, as `@timed()` is, while the factory stays one site and a plain reference none', run: bareDecoratorIsACallSite }, { name: 'declaration extensions are whole extensions', proves: '`.d.cts` and `.d.mts` are single extensions, so no stem keeps a stray `.d`', run: declarationExtensionsAreWholeExtensions }, { name: 'an annotation can declare a signature set', proves: 'a type literal holding several call signatures is resolved as the set it is, and arity picks the arm tsc picks', run: annotationCanDeclareASignatureSet }, { name: 'an uncheckable annotation leaves the call unresolved', proves: 'a call through a variable whose annotation needs the checker names neither the initialiser nor a single candidate', run: uncheckableAnnotationLeavesCallUnresolved },