diff --git a/packages/db/src/hierarchy-writer-coverage.test.ts b/packages/db/src/hierarchy-writer-coverage.test.ts index 4c9ee996..77f13ee6 100644 --- a/packages/db/src/hierarchy-writer-coverage.test.ts +++ b/packages/db/src/hierarchy-writer-coverage.test.ts @@ -79,8 +79,20 @@ * residual is accepted and reviews of DI provider modules carry it. * - Computed member access with a NON-literal name (obj[verb]()) is not * statically resolvable; literal computed access (obj['insert']()) is - * flagged. Constructing the verb at runtime is adjacent to eval and is - * expected to be caught in review. + * flagged, and a computed member CALL whose key is a text-only template + * literal (obj[`insert`]()) fails closed anywhere — template text never + * reaches the lexer's code output, so such a call is indistinguishable + * from a runtime-constructed verb (a template key WITH interpolation is + * a non-literal computed member, above). Constructing the verb at + * runtime is adjacent to eval and is expected to be caught in review. + * - The DB_FACTORY_IMPORTERS enumeration counts the import edges + * hasSymbolImportEdge can see (named import, literal dynamic package + * import with symbol use, namespace member use). Destructuring a factory + * out of a schema namespace (`const { createDb } = dbns`) confers + * capability for prong (iii) but is NOT visible to the enumeration, so a + * file on that route joins the importer set only via review; prong (iii) + * still flags any execution in it. The enumeration is a measured set, + * not a soundness claim. * - General VALUE FLOW is not modeled. The analyzer tracks import edges, * re-export chains, and single-step alias/destructure derivations from a * namespace binding (to depth 2) — not arbitrary assignment chains, @@ -456,9 +468,11 @@ const MEMBER_SEG = `(?:${DOT}\\w+|${BRACKET_OPEN}\\s*['"\`]\\w+['"\`]\\s*\\])`; const memberTail = (alt: string): string => `(?:${DOT}(?:${alt})\\b|${BRACKET_OPEN}\\s*['"\`](?:${alt})['"\`]\\s*\\])`; /** - * Reduce an exported expression to its core: strip parenthesization and - * trailing type assertions (`(companies)`, `companies as unknown as object`) - * so `export default ` passes see the binding under the dressing. + * Reduce an exported expression to its core: strip parenthesization, + * trailing type assertions (`as …`, `satisfies …`), and trailing non-null + * assertions (`(companies)`, `companies as unknown as object`, + * `companies satisfies object`, `companies!`, `(companies as any)!`) so + * `export default ` passes see the binding under the dressing. */ function stripExprDressing(raw: string): string { let expr = raw.trim(); @@ -468,11 +482,16 @@ function stripExprDressing(raw: string): string { .replace(/^\(\s*/, '') .replace(/\s*\)$/, '') .replace(/\s+as\s+[^()]+$/, '') + .replace(/\s+satisfies\s+[^()]+$/, '') + .replace(/\s*!+$/, '') .trim(); } return expr; } +/** Call-open shape tolerant of the optional-call form: `f(…)` or `f?.(…)`. */ +const CALL_OPEN = `\\s*(?:\\?\\.)?\\s*\\(`; + interface FileFacts { rel: string; code: string; @@ -572,7 +591,7 @@ function computeSchemaConduits(files: FileFacts[], fileSet: Set): Schema for (const ns of aliases.namespaces) { for (const m of f.code.matchAll( new RegExp( - `export\\s+(?:const|let|var)\\s+(\\w+)\\s*=\\s*${ns}${MEMBER_SEG}*${memberTail(CLASS_SYMBOLS.join('|'))}`, + `export\\s+(?:const|let|var)\\s+(\\w+)\\s*=\\s*\\(*\\s*${ns}${MEMBER_SEG}*${memberTail(CLASS_SYMBOLS.join('|'))}`, 'g', ), )) { @@ -587,8 +606,9 @@ function computeSchemaConduits(files: FileFacts[], fileSet: Set): Schema } } // The default slot is an export name like any other, whatever the - // expression dressing: bare local, parenthesized, type-asserted, or - // a namespace member chain in dot or bracket form, semicolon or not. + // expression dressing: bare local, parenthesized, type-asserted + // (`as`/`satisfies`), non-null-asserted (`companies!`), or a + // namespace member chain in dot or bracket form, semicolon or not. for (const m of f.code.matchAll(/export\s+default\s+([^;\n]+)/g)) { const expr = stripExprDressing(m[1]!); if (/^\w+$/.test(expr)) { @@ -714,7 +734,7 @@ function classAliases(f: FileFacts, conduits: SchemaConduits, fileSet: Set(); - if (/\bcreateRequire\s*\(/.test(code)) crNames.add('createRequire'); + if (new RegExp(`\\bcreateRequire${CALL_OPEN}`).test(code)) crNames.add('createRequire'); for (const m of code.matchAll(/import\s*\{([^}]*)\}\s*from\s*['"](?:node:)?module['"]/g)) { const alias = /\bcreateRequire\s+as\s+(\w+)/.exec(m[1]!); if (alias) crNames.add(alias[1]!); @@ -1006,7 +1045,7 @@ function analyzeFile(f: FileFacts, ctx: AnalysisCtx): Violation[] { } } for (const n of crNames) { - if (n === 'createRequire' || new RegExp(`\\b${n}\\s*\\(`).test(code)) { + if (n === 'createRequire' || new RegExp(`\\b${n}${CALL_OPEN}`).test(code)) { violations.push({ file: rel, prong: 'dynamic-import', detail: 'createRequire()' }); break; } @@ -1033,7 +1072,7 @@ function analyzeFile(f: FileFacts, ctx: AnalysisCtx): Violation[] { // The argument prefix tolerates parenthesization, spread, and array // wrapping: .insert((companies)), .insert(...[companies]). const writeRe = new RegExp( - `\\.\\s*(insert|update|delete)\\s*\\(\\s*(?:(?:\\(|\\[|\\.\\.\\.)\\s*)*(${targets.join('|')})`, + `\\.\\s*(insert|update|delete)${CALL_OPEN}\\s*(?:(?:\\(|\\[|\\.\\.\\.)\\s*)*(${targets.join('|')})`, 'g', ); for (const m of code.matchAll(writeRe)) { @@ -1051,7 +1090,10 @@ function analyzeFile(f: FileFacts, ctx: AnalysisCtx): Violation[] { // form (measured), so it fails outright. Write verbs fail outside the // allowlist; execution verbs fail outside allowlist ∪ register. if (!inAllowlist) { - const writeBracket = /\[\s*['"](insert|update|delete)['"]\s*\]\s*\(/g; + const writeBracket = new RegExp( + `\\[\\s*['"](insert|update|delete)['"]\\s*\\]${CALL_OPEN}`, + 'g', + ); for (const m of code.matchAll(writeBracket)) { violations.push({ file: rel, @@ -1060,7 +1102,9 @@ function analyzeFile(f: FileFacts, ctx: AnalysisCtx): Violation[] { }); } if (!inRegister) { - for (const m of code.matchAll(/\[\s*['"](execute|query|unsafe)['"]\s*\]\s*\(/g)) { + for (const m of code.matchAll( + new RegExp(`\\[\\s*['"](execute|query|unsafe)['"]\\s*\\]${CALL_OPEN}`, 'g'), + )) { violations.push({ file: rel, prong: 'iii-raw-execution', @@ -1142,8 +1186,9 @@ function analyzeFile(f: FileFacts, ctx: AnalysisCtx): Violation[] { if (driverImport) { violations.push({ file: rel, prong: 'iii-raw-execution', detail: 'direct driver import' }); // A driver client exposes query/execute/unsafe as raw primitives: - // flag them all on any receiver, any argument, spaced or not. - for (const m of code.matchAll(/\.\s*(execute|query|unsafe)\s*\(/g)) { + // flag them all on any receiver, any argument, spaced, optional-call + // (`?.(`) or not. + for (const m of code.matchAll(new RegExp(`\\.\\s*(execute|query|unsafe)${CALL_OPEN}`, 'g'))) { violations.push({ file: rel, prong: 'iii-raw-execution', @@ -1155,7 +1200,7 @@ function analyzeFile(f: FileFacts, ctx: AnalysisCtx): Violation[] { // the relational builder, and unrelated .query() methods are common), // so factory capability flags execute/unsafe on any receiver and query // only on the db/client-shaped receiver backstop below. - for (const m of code.matchAll(/\.\s*(execute|unsafe)\s*\(/g)) { + for (const m of code.matchAll(new RegExp(`\\.\\s*(execute|unsafe)${CALL_OPEN}`, 'g'))) { violations.push({ file: rel, prong: 'iii-raw-execution', @@ -1171,7 +1216,7 @@ function analyzeFile(f: FileFacts, ctx: AnalysisCtx): Violation[] { // access tolerates ?. and ! markers (this.db?.query(…)). for (const m of code.matchAll( new RegExp( - `(?:\\b(?:db|database|client|conn|connection|pool|pg|pglite|tx|trx)|\\[\\s*['"](?:db|database|client|conn|connection|pool|pg|pglite|tx|trx)['"]\\s*\\])${DOT}(execute|query|unsafe)\\s*\\(`, + `(?:\\b(?:db|database|client|conn|connection|pool|pg|pglite|tx|trx)|\\[\\s*['"](?:db|database|client|conn|connection|pool|pg|pglite|tx|trx)['"]\\s*\\])${DOT}(execute|query|unsafe)${CALL_OPEN}`, 'g', ), )) { @@ -1199,7 +1244,7 @@ function analyzeFile(f: FileFacts, ctx: AnalysisCtx): Violation[] { if (nsName) sqlAliases.add(`${nsName}\\s*\\.\\s*sql`); } if (sqlAliases.size > 0) { - const rawRe = new RegExp(`\\b(${[...sqlAliases].join('|')})\\s*\\.\\s*raw\\s*\\(`); + const rawRe = new RegExp(`\\b(${[...sqlAliases].join('|')})\\s*\\.\\s*raw${CALL_OPEN}`); if (rawRe.test(code)) { violations.push({ file: rel, prong: 'iii-raw-execution', detail: 'sql.raw()' }); } @@ -1749,6 +1794,112 @@ describe('hierarchy writer coverage (contract 1 §6.3b)', () => { name: 'E68 optional-chained bracketed DI receiver', src: `export class R { async f(t: string) { await this['db']?.query('TRUNCATE ' + t); } }`, }, + { + name: 'E69 non-null-asserted default export of schema symbol', + src: `import c from './evasion-mid26.js';\nimport { db } from './x.js';\nexport async function f() { await db.insert(c).values({}); }`, + extras: [ + { + rel: 'packages/db/src/evasion-mid26.ts', + src: `import { companies } from '@mosaicstack/db';\nexport default companies!;`, + }, + ], + }, + { + name: 'E70 satisfies-dressed default export of schema symbol', + src: `import c from './evasion-mid27.js';\nimport { db } from './x.js';\nexport async function f() { await db.insert(c).values({}); }`, + extras: [ + { + rel: 'packages/db/src/evasion-mid27.ts', + src: `import { companies } from '@mosaicstack/db';\nexport default companies satisfies object;`, + }, + ], + }, + { + name: 'E71 paren-plus-assertion-plus-non-null default export', + src: `import c from './evasion-mid28.js';\nimport { db } from './x.js';\nexport async function f() { await db.insert(c).values({}); }`, + extras: [ + { + rel: 'packages/db/src/evasion-mid28.ts', + src: `import { companies } from '@mosaicstack/db';\nexport default (companies as unknown)!;`, + }, + ], + }, + { + name: 'E72 non-null-asserted ns-member default export (dynamic ns)', + src: `import c from './evasion-mid29.js';\nimport { db } from './x.js';\nexport async function f() { await db.insert(c).values({}); }`, + extras: [ + { + rel: 'packages/db/src/evasion-mid29.ts', + src: `const mod = await import('@mosaicstack/db');\nexport default mod.companies!;`, + }, + ], + }, + { + name: 'E73 parenthesized ns-member export-const of schema symbol', + src: `import { co } from './evasion-mid30.js';\nimport { db } from './x.js';\nexport async function f() { await db.insert(co).values({}); }`, + extras: [ + { + rel: 'packages/db/src/evasion-mid30.ts', + src: `import * as ns from '@mosaicstack/db';\nexport const co = (ns.companies);`, + }, + ], + }, + { + name: 'E74 non-null-asserted default export of factory local', + src: `import mk from './evasion-mid31.js';\nexport async function f(t: string) { await mk('u').execute('DELETE FROM ' + t); }`, + extras: [ + { + rel: 'packages/db/src/evasion-mid31.ts', + src: `import { createDb } from '@mosaicstack/db';\nexport default createDb!;`, + }, + ], + }, + { + name: 'E75 non-null-asserted ns-member factory default export', + src: `import mk from './evasion-mid32.js';\nexport async function f(t: string) { await mk('u').execute('DELETE FROM ' + t); }`, + extras: [ + { + rel: 'packages/db/src/evasion-mid32.ts', + src: `import * as ns from '@mosaicstack/db';\nexport default ns.createDb!;`, + }, + ], + }, + { + name: 'E76 satisfies-dressed default export of factory local', + src: `import mk from './evasion-mid33.js';\nexport async function f(t: string) { await mk('u').execute('DELETE FROM ' + t); }`, + extras: [ + { + rel: 'packages/db/src/evasion-mid33.ts', + src: `import { createDb } from '@mosaicstack/db';\nexport default createDb satisfies typeof createDb;`, + }, + ], + }, + { + name: 'E77 parenthesized ns-member factory derivation re-exported', + src: `import mk from './evasion-mid34.js';\nexport async function f(t: string) { await mk('u').execute('DELETE FROM ' + t); }`, + extras: [ + { + rel: 'packages/db/src/evasion-mid34.ts', + src: `import * as ns from '@mosaicstack/db';\nconst mk = (ns.createDb);\nexport default mk;`, + }, + ], + }, + { + name: 'E78 optional-call write verb', + src: `import { companies } from '@mosaicstack/db';\nimport { db } from './x.js';\nexport async function f() { await db.insert?.(companies).values({}); }`, + }, + { + name: 'E79 optional-call DI receiver chain', + src: `export class R { constructor(private pool?: { execute(s: string): Promise }) {}\n async f(t: string) { await this.pool?.execute?.('TRUNCATE ' + t); } }`, + }, + { + name: 'E80 optional-call execution in factory-capable file', + src: `import { createDb } from '@mosaicstack/db';\nexport async function f(t: string) { const d = createDb('u'); await d.execute?.('DELETE FROM ' + t); }`, + }, + { + name: 'E81 template-literal computed member call', + src: `import { companies } from '@mosaicstack/db';\nimport { db } from './x.js';\nexport async function f() { await db[\`insert\`](companies).values({}); }`, + }, ]; const CLEAN_CONTROLS: Array<{ name: string; src: string }> = [ {