fix(db): propagate symbol identity through conduits in writer-coverage assertion
ci/woodpecker/pr/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful
Round-3 review remediation (G1-G6):
- export-map schema conduits: renames propagate through export-from,
export-of-local, and export-const-from-namespace routes (G1)
- fail-closed namespace destructure over-approximation + depth-2 alias
derivation (const t = ns.companies, const { schema } = ns) (G1)
- factory capability: consumer-side name gate dropped for conduit imports;
export-of-local factory bindings make a module a factory conduit (G2)
- createRequire tracked through import aliases and dynamic destructure (G3)
- literal dynamic import of a schema/factory/driver source outside the
tracked const-await binding shape is a violation (.then/deferred/
Promise.all) (G4)
- prong (ii): quoted schema qualifier and SQL line-comment gaps (G5)
- computed eval, spaced execution verbs, bracket-form and parenthesized
write arguments, spaced sql.raw (G6)
- value-flow residual documented in KNOWN RESIDUALS with counterfactuals
- 16 new permanent controls (E24-E39)
This commit is contained in:
@@ -9,9 +9,17 @@
|
|||||||
* (i) Symbol prong — write references (insert/update/delete) to the
|
* (i) Symbol prong — write references (insert/update/delete) to the
|
||||||
* class-table schema symbols occur only in allowlisted modules.
|
* class-table schema symbols occur only in allowlisted modules.
|
||||||
* Schema symbols are tracked through named imports (aliased or not),
|
* Schema symbols are tracked through named imports (aliased or not),
|
||||||
* namespace imports, and re-export conduits: any scanned module that
|
* namespace imports, and re-export conduits. Conduits carry an
|
||||||
* re-exports the schema (or another conduit) is itself treated as a
|
* EXPORT MAP (per-module named exports and namespace exports),
|
||||||
* schema source, computed to a fixpoint.
|
* computed to a fixpoint, so a rename at the export site
|
||||||
|
* (`export { companies as c } from`), an export of a locally bound
|
||||||
|
* alias (`import { companies }; export { companies as co }`), and a
|
||||||
|
* binding derived from a namespace (`export const co = ns.companies`)
|
||||||
|
* all propagate symbol identity to the consumer. Destructuring a
|
||||||
|
* NON-class property from a schema namespace over-approximates: the
|
||||||
|
* binding is treated as a schema namespace itself (fail-closed), and
|
||||||
|
* single-alias derivation from a namespace (`const t = ns.companies`)
|
||||||
|
* is folded in to a fixed depth.
|
||||||
* (ii) Literal prong — a class-table name inside a string or template
|
* (ii) Literal prong — a class-table name inside a string or template
|
||||||
* span that also carries SQL context fails outside the allowlist.
|
* span that also carries SQL context fails outside the allowlist.
|
||||||
* Spans are produced by a real lexer, so comments cannot hide code
|
* Spans are produced by a real lexer, so comments cannot hide code
|
||||||
@@ -28,14 +36,23 @@
|
|||||||
* receiver backstop still fires on db/client-shaped receivers.
|
* receiver backstop still fires on db/client-shaped receivers.
|
||||||
* `sql.raw` is tracked through import aliasing and namespaces.
|
* `sql.raw` is tracked through import aliasing and namespaces.
|
||||||
*
|
*
|
||||||
* Runtime code-construction primitives (eval, new Function) fail anywhere —
|
* Runtime code-construction primitives (eval, new Function — spelled
|
||||||
* allowlist and register included: constructed code defeats every static
|
* directly or as a literal computed member like `globalThis['eval']`) fail
|
||||||
* prong, so there is no enumerated disposition path for it. Unanalyzable
|
* anywhere — allowlist and register included: constructed code defeats every
|
||||||
* import routes (a dynamic import whose specifier is not a single string
|
* static prong, so there is no enumerated disposition path for it.
|
||||||
* literal, and createRequire) fail everywhere except their own enumerated
|
* Unanalyzable import routes fail everywhere except their own enumerated
|
||||||
* disposition registers below. A LITERAL dynamic import is an import edge
|
* disposition registers below: a dynamic import whose specifier is not a
|
||||||
* like any static import (contract rev 9): its bindings (destructured or
|
* single string literal, and createRequire (tracked through import aliasing
|
||||||
* namespace) feed prong (i) exactly as static import bindings do.
|
* — `createRequire as x` — and destructuring from the module namespace; a
|
||||||
|
* route that hides the NAME entirely, e.g. re-exporting createRequire from a
|
||||||
|
* helper, falls under the value-flow residual). A LITERAL dynamic import is
|
||||||
|
* an import edge like any static import (contract rev 9): its bindings
|
||||||
|
* (destructured or namespace) feed prong (i) exactly as static import
|
||||||
|
* bindings do — and to keep that claim sound, a literal dynamic import of a
|
||||||
|
* schema/factory/driver source OUTSIDE the tracked
|
||||||
|
* `const X = await import('…')` binding shape (`.then` chains, deferred
|
||||||
|
* awaits, Promise.all) is itself a violation: bindings the analyzer cannot
|
||||||
|
* track are not allowed to exist for capability-bearing modules.
|
||||||
*
|
*
|
||||||
* KNOWN RESIDUALS (deliberate, reviewed trade-offs — not claims of closure):
|
* KNOWN RESIDUALS (deliberate, reviewed trade-offs — not claims of closure):
|
||||||
* - In a file with NO detected raw capability (no driver/factory import on
|
* - In a file with NO detected raw capability (no driver/factory import on
|
||||||
@@ -50,6 +67,17 @@
|
|||||||
* statically resolvable; literal computed access (obj['insert']()) is
|
* statically resolvable; literal computed access (obj['insert']()) is
|
||||||
* flagged. Constructing the verb at runtime is adjacent to eval and is
|
* flagged. Constructing the verb at runtime is adjacent to eval and is
|
||||||
* expected to be caught in review.
|
* expected to be caught in review.
|
||||||
|
* - 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,
|
||||||
|
* function returns, or method extraction. Demonstrated escapes in this
|
||||||
|
* class: `const u = this.client.unsafe; u.call(this.client, s)` (the
|
||||||
|
* verb never appears as a member call), an alias chain three or more
|
||||||
|
* steps deep, and a helper function that returns a schema symbol.
|
||||||
|
* Closing it requires data-flow analysis (a type-checker-backed rewrite,
|
||||||
|
* tracked for M4-1b consideration); the counterfactual — flagging every
|
||||||
|
* bare identifier call — false-positives on essentially all callback
|
||||||
|
* code. Reviews of modules touching db handles carry this residual.
|
||||||
* - The scan perimeter is <root>/<pkg>/src for the three roots; production
|
* - The scan perimeter is <root>/<pkg>/src for the three roots; production
|
||||||
* TS outside a src/ directory (e.g. packages/mosaic/framework/**) is not
|
* TS outside a src/ directory (e.g. packages/mosaic/framework/**) is not
|
||||||
* scanned (verified free of db/driver/execute references at review time).
|
* scanned (verified free of db/driver/execute references at review time).
|
||||||
@@ -415,46 +443,109 @@ interface Violation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compute the fixpoint set of "schema sources": module paths from which a
|
* Schema exports of one module in the conduit graph: `named` are exported
|
||||||
* class-table symbol can be imported. Seeds: the schema module and the db
|
* identifiers bound to a class-table symbol (under WHATEVER exported name —
|
||||||
* package barrel (plus the bare '@mosaicstack/db' specifier, handled
|
* renames propagate); `ns` are exported identifiers that are themselves
|
||||||
* separately). Any scanned module that re-exports from a schema source
|
* namespaces over a schema source (`export * as x from …`).
|
||||||
* (star, or a named list carrying a class symbol) — or imports class symbols
|
|
||||||
* and re-exports those local names — joins the set.
|
|
||||||
*/
|
*/
|
||||||
function computeSchemaConduits(files: FileFacts[], fileSet: Set<string>): Set<string> {
|
interface SchemaExports {
|
||||||
const conduits = new Set<string>(['packages/db/src/schema.ts', 'packages/db/src/index.ts']);
|
named: Set<string>;
|
||||||
const isSchemaSpec = (rel: string, spec: string): boolean => {
|
ns: Set<string>;
|
||||||
if (spec === '@mosaicstack/db') return true;
|
}
|
||||||
const resolved = resolveSpecifier(rel, spec, fileSet);
|
type SchemaConduits = Map<string, SchemaExports>;
|
||||||
return resolved !== null && conduits.has(resolved);
|
|
||||||
};
|
/** Exported class-symbol names reachable through `spec` from `rel` (null = not a schema source). */
|
||||||
|
function schemaExportsOf(
|
||||||
|
rel: string,
|
||||||
|
spec: string,
|
||||||
|
conduits: SchemaConduits,
|
||||||
|
fileSet: Set<string>,
|
||||||
|
): SchemaExports | null {
|
||||||
|
if (spec === '@mosaicstack/db') return { named: new Set(CLASS_SYMBOLS), ns: new Set() };
|
||||||
|
const r = resolveSpecifier(rel, spec, fileSet);
|
||||||
|
return r !== null ? (conduits.get(r) ?? null) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute the fixpoint map of "schema sources": module → the exported names
|
||||||
|
* under which a class-table symbol (or a schema namespace) is reachable from
|
||||||
|
* it. Seeds: the schema module and the db package barrel (the bare
|
||||||
|
* '@mosaicstack/db' specifier is handled in schemaExportsOf). Renames are
|
||||||
|
* propagated: `export { companies as c } from …` exports `c`, an
|
||||||
|
* `export { x as y }` of a local class binding exports `y`, and
|
||||||
|
* `export const y = ns.companies` over a schema namespace exports `y`.
|
||||||
|
*/
|
||||||
|
function computeSchemaConduits(files: FileFacts[], fileSet: Set<string>): SchemaConduits {
|
||||||
|
const conduits: SchemaConduits = new Map([
|
||||||
|
['packages/db/src/schema.ts', { named: new Set(CLASS_SYMBOLS), ns: new Set<string>() }],
|
||||||
|
['packages/db/src/index.ts', { named: new Set(CLASS_SYMBOLS), ns: new Set<string>() }],
|
||||||
|
]);
|
||||||
let changed = true;
|
let changed = true;
|
||||||
while (changed) {
|
while (changed) {
|
||||||
changed = false;
|
changed = false;
|
||||||
for (const f of files) {
|
for (const f of files) {
|
||||||
if (conduits.has(f.rel)) continue;
|
const mine: SchemaExports = conduits.get(f.rel) ?? {
|
||||||
let isConduit = false;
|
named: new Set<string>(),
|
||||||
|
ns: new Set<string>(),
|
||||||
|
};
|
||||||
|
const before = mine.named.size + mine.ns.size;
|
||||||
for (const m of f.code.matchAll(EXPORT_FROM_RE)) {
|
for (const m of f.code.matchAll(EXPORT_FROM_RE)) {
|
||||||
if (m[1]) continue; // export type — erased
|
if (m[1]) continue; // export type — erased
|
||||||
if (!isSchemaSpec(f.rel, m[3]!)) continue;
|
const src = schemaExportsOf(f.rel, m[3]!, conduits, fileSet);
|
||||||
if (m[2] === undefined) {
|
if (src === null || (src.named.size === 0 && src.ns.size === 0)) continue;
|
||||||
isConduit = true; // export * from schema source
|
const starAs = /export\s*\*\s*as\s+(\w+)/.exec(m[0]);
|
||||||
} else if (CLASS_SYMBOLS.some((s) => new RegExp(`\\b${s}\\b`).test(m[2]!))) {
|
if (starAs) {
|
||||||
isConduit = true;
|
mine.ns.add(starAs[1]!); // export * as x from schema source
|
||||||
|
} else if (m[2] === undefined) {
|
||||||
|
for (const n of src.named) mine.named.add(n); // export * from …
|
||||||
|
for (const n of src.ns) mine.ns.add(n);
|
||||||
|
} else {
|
||||||
|
for (const part of m[2].split(',')) {
|
||||||
|
const seg = part.trim();
|
||||||
|
if (!seg || seg.startsWith('type ')) continue;
|
||||||
|
const asMatch = /^(\w+)\s+as\s+(\w+)$/.exec(seg);
|
||||||
|
const original = asMatch ? asMatch[1]! : seg;
|
||||||
|
const exported = asMatch ? asMatch[2]! : seg;
|
||||||
|
if (src.named.has(original)) mine.named.add(exported);
|
||||||
|
if (src.ns.has(original)) mine.ns.add(exported);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!isConduit) {
|
// Exports of local bindings: `export { x as y }` where x is a local
|
||||||
const aliases = classAliases(f, conduits, fileSet);
|
// class alias (or namespace), and `export const y = ns.<classSym>`.
|
||||||
if (aliases.named.length > 0) {
|
const aliases = classAliases(f, conduits, fileSet);
|
||||||
const exported = [...f.code.matchAll(/export\s*\{([^}]*)\}(?!\s*from)/g)]
|
if (aliases.named.length > 0 || aliases.namespaces.length > 0) {
|
||||||
.map((m) => m[1]!)
|
for (const m of f.code.matchAll(/export\s*\{([^}]*)\}(?!\s*from)/g)) {
|
||||||
.join(',');
|
for (const part of m[1]!.split(',')) {
|
||||||
if (aliases.named.some((a) => new RegExp(`\\b${a}\\b`).test(exported))) isConduit = true;
|
const seg = part.trim();
|
||||||
|
if (!seg || seg.startsWith('type ')) continue;
|
||||||
|
const asMatch = /^(\w+)\s+as\s+(\w+)$/.exec(seg);
|
||||||
|
const local = asMatch ? asMatch[1]! : seg;
|
||||||
|
const exported = asMatch ? asMatch[2]! : seg;
|
||||||
|
if (aliases.named.includes(local)) mine.named.add(exported);
|
||||||
|
if (aliases.namespaces.includes(local)) mine.ns.add(exported);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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}(?:\\s*\\.\\s*\\w+)*\\s*\\.\\s*(?:${CLASS_SYMBOLS.join('|')})\\b`,
|
||||||
|
'g',
|
||||||
|
),
|
||||||
|
)) {
|
||||||
|
mine.named.add(m[1]!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const local of aliases.named) {
|
||||||
|
for (const m of f.code.matchAll(
|
||||||
|
new RegExp(`export\\s+(?:const|let|var)\\s+(\\w+)\\s*=\\s*${local}\\b`, 'g'),
|
||||||
|
)) {
|
||||||
|
mine.named.add(m[1]!);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (isConduit) {
|
if (mine.named.size + mine.ns.size > before) {
|
||||||
conduits.add(f.rel);
|
conduits.set(f.rel, mine);
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -467,48 +558,60 @@ interface ClassAliases {
|
|||||||
namespaces: string[]; // namespace identifiers over a schema source
|
namespaces: string[]; // namespace identifiers over a schema source
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Parse an import named-binding list ("a, b as c") into locals bound to class symbols. */
|
/**
|
||||||
function importBindings(namedList: string, named: Set<string>): void {
|
* Parse an import named-binding list ("a, b as c") against the SOURCE's
|
||||||
|
* schema exports, adding locals bound to class symbols / schema namespaces.
|
||||||
|
*/
|
||||||
|
function importBindings(
|
||||||
|
namedList: string,
|
||||||
|
src: SchemaExports,
|
||||||
|
named: Set<string>,
|
||||||
|
namespaces: Set<string>,
|
||||||
|
): void {
|
||||||
for (const part of namedList.split(',')) {
|
for (const part of namedList.split(',')) {
|
||||||
const seg = part.trim();
|
const seg = part.trim();
|
||||||
if (!seg || seg.startsWith('type ')) continue;
|
if (!seg || seg.startsWith('type ')) continue;
|
||||||
const asMatch = /^(\w+)\s+as\s+(\w+)$/.exec(seg);
|
const asMatch = /^(\w+)\s+as\s+(\w+)$/.exec(seg);
|
||||||
const original = asMatch ? asMatch[1]! : seg;
|
const original = asMatch ? asMatch[1]! : seg;
|
||||||
const local = asMatch ? asMatch[2]! : seg;
|
const local = asMatch ? asMatch[2]! : seg;
|
||||||
if (CLASS_SYMBOLS.includes(original)) named.add(local);
|
if (src.named.has(original)) named.add(local);
|
||||||
|
if (src.ns.has(original)) namespaces.add(local);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Parse a destructuring pattern ("a, b: c") into locals bound to class symbols. */
|
/**
|
||||||
function destructureBindings(pattern: string, named: Set<string>): void {
|
* Parse a destructuring pattern ("a, b: c, d = x") over a schema source.
|
||||||
|
* A class-symbol property binds a named alias; ANY other destructured
|
||||||
|
* property is over-approximated as a schema namespace (it may be a nested
|
||||||
|
* namespace such as `const { schema } = ns` — fail-closed).
|
||||||
|
*/
|
||||||
|
function destructureBindings(
|
||||||
|
pattern: string,
|
||||||
|
src: SchemaExports,
|
||||||
|
named: Set<string>,
|
||||||
|
namespaces: Set<string>,
|
||||||
|
): void {
|
||||||
for (const part of pattern.split(',')) {
|
for (const part of pattern.split(',')) {
|
||||||
const seg = part.trim();
|
const seg = part.trim();
|
||||||
if (!seg) continue;
|
if (!seg) continue;
|
||||||
const m = /^(\w+)\s*(?::\s*(\w+))?$/.exec(seg);
|
const m = /^(\w+)\s*(?::\s*(\w+))?\s*(?:=[\s\S]*)?$/.exec(seg);
|
||||||
if (m && CLASS_SYMBOLS.includes(m[1]!)) named.add(m[2] ?? m[1]!);
|
if (!m) continue;
|
||||||
|
const local = m[2] ?? m[1]!;
|
||||||
|
if (src.named.has(m[1]!)) named.add(local);
|
||||||
|
else namespaces.add(local);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function isSchemaSpecifier(
|
function classAliases(f: FileFacts, conduits: SchemaConduits, fileSet: Set<string>): ClassAliases {
|
||||||
rel: string,
|
|
||||||
spec: string,
|
|
||||||
conduits: Set<string>,
|
|
||||||
fileSet: Set<string>,
|
|
||||||
): boolean {
|
|
||||||
if (spec === '@mosaicstack/db') return true;
|
|
||||||
const r = resolveSpecifier(rel, spec, fileSet);
|
|
||||||
return r !== null && conduits.has(r);
|
|
||||||
}
|
|
||||||
|
|
||||||
function classAliases(f: FileFacts, conduits: Set<string>, fileSet: Set<string>): ClassAliases {
|
|
||||||
const named = new Set<string>();
|
const named = new Set<string>();
|
||||||
const namespaces = new Set<string>();
|
const namespaces = new Set<string>();
|
||||||
for (const m of f.code.matchAll(IMPORT_RE)) {
|
for (const m of f.code.matchAll(IMPORT_RE)) {
|
||||||
const [, typeOnly, , namedList, nsName, , spec] = m;
|
const [, typeOnly, , namedList, nsName, , spec] = m;
|
||||||
if (typeOnly) continue;
|
if (typeOnly) continue;
|
||||||
if (!isSchemaSpecifier(f.rel, spec!, conduits, fileSet)) continue;
|
const src = schemaExportsOf(f.rel, spec!, conduits, fileSet);
|
||||||
|
if (src === null || (src.named.size === 0 && src.ns.size === 0)) continue;
|
||||||
if (nsName) namespaces.add(nsName);
|
if (nsName) namespaces.add(nsName);
|
||||||
if (namedList) importBindings(namedList, named);
|
if (namedList) importBindings(namedList, src, named, namespaces);
|
||||||
}
|
}
|
||||||
// Literal dynamic imports of a schema source are import edges like any
|
// Literal dynamic imports of a schema source are import edges like any
|
||||||
// other (contract rev 9): both binding shapes feed prong (i).
|
// other (contract rev 9): both binding shapes feed prong (i).
|
||||||
@@ -516,17 +619,33 @@ function classAliases(f: FileFacts, conduits: Set<string>, fileSet: Set<string>)
|
|||||||
/(?:const|let|var)\s*(?:\{([^}]*)\}|(\w+))\s*=\s*await\s+import\s*\(\s*(['"])([^'"]+)\3\s*\)/g,
|
/(?:const|let|var)\s*(?:\{([^}]*)\}|(\w+))\s*=\s*await\s+import\s*\(\s*(['"])([^'"]+)\3\s*\)/g,
|
||||||
)) {
|
)) {
|
||||||
const [, pattern, nsName, , spec] = m;
|
const [, pattern, nsName, , spec] = m;
|
||||||
if (!isSchemaSpecifier(f.rel, spec!, conduits, fileSet)) continue;
|
const src = schemaExportsOf(f.rel, spec!, conduits, fileSet);
|
||||||
|
if (src === null || (src.named.size === 0 && src.ns.size === 0)) continue;
|
||||||
if (nsName) namespaces.add(nsName);
|
if (nsName) namespaces.add(nsName);
|
||||||
if (pattern) destructureBindings(pattern, named);
|
if (pattern) destructureBindings(pattern, src, named, namespaces);
|
||||||
}
|
}
|
||||||
// Destructuring from a schema namespace binds class symbols to locals:
|
// Aliases derived FROM a schema namespace, to a bounded depth (2 passes):
|
||||||
// `import * as s from '@mosaicstack/db'; const { companies } = s;`
|
// const { companies } = ns; → named alias
|
||||||
for (const ns of [...namespaces]) {
|
// const { schema } = ns; → nested namespace (fail-closed)
|
||||||
for (const m of f.code.matchAll(
|
// const t = ns.companies; → named alias
|
||||||
new RegExp(`(?:const|let|var)\\s*\\{([^}]*)\\}\\s*=\\s*${ns}\\b`, 'g'),
|
// const s2 = ns.schema; → namespace alias
|
||||||
)) {
|
const nsSrc: SchemaExports = { named: new Set(CLASS_SYMBOLS), ns: new Set() };
|
||||||
destructureBindings(m[1]!, named);
|
for (let pass = 0; pass < 2; pass += 1) {
|
||||||
|
for (const ns of [...namespaces]) {
|
||||||
|
for (const m of f.code.matchAll(
|
||||||
|
new RegExp(`(?:const|let|var)\\s*\\{([^}]*)\\}\\s*=\\s*(?:await\\s+)?${ns}\\b`, 'g'),
|
||||||
|
)) {
|
||||||
|
destructureBindings(m[1]!, nsSrc, named, namespaces);
|
||||||
|
}
|
||||||
|
for (const m of f.code.matchAll(
|
||||||
|
new RegExp(
|
||||||
|
`(?:const|let|var)\\s+(\\w+)\\s*=\\s*${ns}(?:\\s*\\.\\s*\\w+)*\\s*\\.\\s*(\\w+)\\b`,
|
||||||
|
'g',
|
||||||
|
),
|
||||||
|
)) {
|
||||||
|
if (CLASS_SYMBOLS.includes(m[2]!)) named.add(m[1]!);
|
||||||
|
else namespaces.add(m[1]!);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { named: [...named], namespaces: [...namespaces] };
|
return { named: [...named], namespaces: [...namespaces] };
|
||||||
@@ -540,8 +659,10 @@ function classAliases(f: FileFacts, conduits: Set<string>, fileSet: Set<string>)
|
|||||||
* capability (the conduit module itself is additionally flagged by prong
|
* capability (the conduit module itself is additionally flagged by prong
|
||||||
* (iii)'s literal check, so these routes fail at both ends).
|
* (iii)'s literal check, so these routes fail at both ends).
|
||||||
* `factory` — re-exports createDb/createPgliteDb from the db package or
|
* `factory` — re-exports createDb/createPgliteDb from the db package or
|
||||||
* another factory conduit; importing from one while referencing a factory
|
* another factory conduit (via `export … from`, or by exporting a local
|
||||||
* symbol confers factory capability.
|
* binding of the factory under any name); importing ANYTHING from one
|
||||||
|
* confers factory capability — the conduit may rename the symbol, so there
|
||||||
|
* is no consumer-side name gate.
|
||||||
*/
|
*/
|
||||||
function computeCapabilityConduits(
|
function computeCapabilityConduits(
|
||||||
files: FileFacts[],
|
files: FileFacts[],
|
||||||
@@ -580,6 +701,58 @@ function computeCapabilityConduits(
|
|||||||
}
|
}
|
||||||
// `export { default as x } from 'postgres'` matches EXPORT_FROM_RE's
|
// `export { default as x } from 'postgres'` matches EXPORT_FROM_RE's
|
||||||
// named branch above; `export x from` is not valid syntax — covered.
|
// named branch above; `export x from` is not valid syntax — covered.
|
||||||
|
// Export-of-local factory bindings: a module that BINDS the factory
|
||||||
|
// (named import, namespace member, or tracked dynamic import) and
|
||||||
|
// exports that binding under any name is a factory conduit even with
|
||||||
|
// no `export … from` clause (`export const mk = mod.createDb`).
|
||||||
|
if (!factory.has(f.rel)) {
|
||||||
|
const locals = new Set<string>();
|
||||||
|
const nss = new Set<string>();
|
||||||
|
for (const im of f.code.matchAll(IMPORT_RE)) {
|
||||||
|
const [, typeOnly, , namedList, nsName, , spec] = im;
|
||||||
|
if (typeOnly || !isFactorySpec(f.rel, spec!)) continue;
|
||||||
|
if (nsName) nss.add(nsName);
|
||||||
|
if (namedList) {
|
||||||
|
for (const part of namedList.split(',')) {
|
||||||
|
const am = /^(createDb|createPgliteDb)(?:\s+as\s+(\w+))?$/.exec(part.trim());
|
||||||
|
if (am) locals.add(am[2] ?? am[1]!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const dm of f.code.matchAll(
|
||||||
|
/(?:const|let|var)\s*(?:\{([^}]*)\}|(\w+))\s*=\s*await\s+import\s*\(\s*(['"])([^'"]+)\3\s*\)/g,
|
||||||
|
)) {
|
||||||
|
const [, pattern, nsName, , spec] = dm;
|
||||||
|
if (!isFactorySpec(f.rel, spec!)) continue;
|
||||||
|
if (nsName) nss.add(nsName);
|
||||||
|
if (pattern) {
|
||||||
|
for (const part of pattern.split(',')) {
|
||||||
|
const pm = /^(createDb|createPgliteDb)\s*(?::\s*(\w+))?/.exec(part.trim());
|
||||||
|
if (pm) locals.add(pm[2] ?? pm[1]!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const ns of nss) {
|
||||||
|
for (const am of f.code.matchAll(
|
||||||
|
new RegExp(
|
||||||
|
`(?:const|let|var)\\s+(\\w+)\\s*=\\s*${ns}\\s*\\.\\s*(?:createDb|createPgliteDb)\\b`,
|
||||||
|
'g',
|
||||||
|
),
|
||||||
|
)) {
|
||||||
|
locals.add(am[1]!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const local of locals) {
|
||||||
|
if (
|
||||||
|
new RegExp(`export\\s+(?:const|let|var|function)\\s+${local}\\b`).test(f.code) ||
|
||||||
|
new RegExp(`export\\s*\\{[^}]*\\b${local}\\b[^}]*\\}`).test(f.code)
|
||||||
|
) {
|
||||||
|
factory.add(f.rel);
|
||||||
|
changed = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { driver, factory };
|
return { driver, factory };
|
||||||
@@ -609,7 +782,7 @@ function hasSymbolImportEdge(code: string, symbols: string[], packageName: strin
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
interface AnalysisCtx {
|
interface AnalysisCtx {
|
||||||
fileSet: Set<string>;
|
fileSet: Set<string>;
|
||||||
conduits: Set<string>; // schema-symbol sources (prong i)
|
conduits: SchemaConduits; // schema-symbol sources with exported names (prong i)
|
||||||
driverConduits: Set<string>; // driver-capability re-exporters (prong iii)
|
driverConduits: Set<string>; // driver-capability re-exporters (prong iii)
|
||||||
factoryConduits: Set<string>; // factory-capability re-exporters (prong iii)
|
factoryConduits: Set<string>; // factory-capability re-exporters (prong iii)
|
||||||
}
|
}
|
||||||
@@ -623,39 +796,90 @@ function analyzeFile(f: FileFacts, ctx: AnalysisCtx): Violation[] {
|
|||||||
const inRegister = INFRA_REGISTER.includes(rel);
|
const inRegister = INFRA_REGISTER.includes(rel);
|
||||||
const isSchemaDefinition = rel === 'packages/db/src/schema.ts';
|
const isSchemaDefinition = rel === 'packages/db/src/schema.ts';
|
||||||
|
|
||||||
// Runtime code construction: fails anywhere.
|
// Runtime code construction: fails anywhere. Covers direct calls and
|
||||||
if (/\beval\s*\(|\bnew\s+Function\s*\(/.test(code)) {
|
// literal computed access (window['eval'], globalThis['Function']).
|
||||||
|
if (/\beval\s*\(|\bnew\s+Function\s*\(|\[\s*['"](eval|Function)['"]\s*\]/.test(code)) {
|
||||||
violations.push({ file: rel, prong: 'code-construction', detail: 'eval/new Function' });
|
violations.push({ file: rel, prong: 'code-construction', detail: 'eval/new Function' });
|
||||||
}
|
}
|
||||||
// Dynamic import whose specifier is not a single string literal: the
|
// Dynamic import whose specifier is not a single string literal: the
|
||||||
// import graph becomes unanalyzable. Checked per call site, so a literal
|
// import graph becomes unanalyzable. Checked per call site, so a literal
|
||||||
// first fragment ('x' + y) does not slip past.
|
// first fragment ('x' + y) does not slip past. A LITERAL import of a
|
||||||
|
// schema/factory source must additionally sit in the tracked binding shape
|
||||||
|
// (const X = await import(…)) — any other consumption of its promise
|
||||||
|
// (.then, deferred await, array wrapping) hides the binding from prong (i),
|
||||||
|
// so it fails closed here.
|
||||||
if (!DYNAMIC_IMPORT_REGISTER.includes(rel)) {
|
if (!DYNAMIC_IMPORT_REGISTER.includes(rel)) {
|
||||||
for (const m of code.matchAll(/\bimport\s*\(/g)) {
|
for (const m of code.matchAll(/\bimport\s*\(/g)) {
|
||||||
const tail = code.slice((m.index ?? 0) + m[0].length);
|
const idx = m.index ?? 0;
|
||||||
if (!/^\s*(['"])(?:[^'"\\]|\\.)*?\1\s*[,)]/.test(tail)) {
|
const tail = code.slice(idx + m[0].length);
|
||||||
|
const lit = /^\s*(['"])((?:[^'"\\]|\\.)*?)\1\s*[,)]/.exec(tail);
|
||||||
|
if (!lit) {
|
||||||
violations.push({ file: rel, prong: 'dynamic-import', detail: 'non-literal import()' });
|
violations.push({ file: rel, prong: 'dynamic-import', detail: 'non-literal import()' });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const spec = lit[2]!;
|
||||||
|
const schemaSrc = schemaExportsOf(rel, spec, conduits, fileSet);
|
||||||
|
const factoryLike =
|
||||||
|
spec === '@mosaicstack/db' ||
|
||||||
|
(() => {
|
||||||
|
const r = resolveSpecifier(rel, spec, fileSet);
|
||||||
|
return r !== null && (factoryConduits.has(r) || driverConduits.has(r));
|
||||||
|
})() ||
|
||||||
|
DRIVER_SPECIFIERS.includes(spec);
|
||||||
|
if (schemaSrc !== null || factoryLike) {
|
||||||
|
const before = code.slice(Math.max(0, idx - 200), idx);
|
||||||
|
if (!/(?:const|let|var)\s*(?:\{[^}]*\}|\w+)\s*=\s*await\s*$/.test(before)) {
|
||||||
|
violations.push({
|
||||||
|
file: rel,
|
||||||
|
prong: 'dynamic-import',
|
||||||
|
detail: `literal import('${spec}') outside the tracked binding shape`,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// createRequire: an unanalyzable CJS import route.
|
// createRequire: an unanalyzable CJS import route. Tracked by name AND
|
||||||
if (!CREATE_REQUIRE_REGISTER.includes(rel) && /\bcreateRequire\s*\(/.test(code)) {
|
// through import aliases (import { createRequire as x } / destructured
|
||||||
violations.push({ file: rel, prong: 'dynamic-import', detail: 'createRequire()' });
|
// from a dynamic module import).
|
||||||
|
if (!CREATE_REQUIRE_REGISTER.includes(rel)) {
|
||||||
|
const crNames = new Set<string>();
|
||||||
|
if (/\bcreateRequire\s*\(/.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]!);
|
||||||
|
}
|
||||||
|
for (const m of code.matchAll(
|
||||||
|
/(?:const|let|var)\s*\{([^}]*)\}\s*=\s*await\s+import\s*\(\s*['"](?:node:)?module['"]\s*\)/g,
|
||||||
|
)) {
|
||||||
|
const alias = /\bcreateRequire\s*:\s*(\w+)/.exec(m[1]!);
|
||||||
|
if (alias) crNames.add(alias[1]!);
|
||||||
|
}
|
||||||
|
for (const n of crNames) {
|
||||||
|
if (n === 'createRequire' || new RegExp(`\\b${n}\\s*\\(`).test(code)) {
|
||||||
|
violations.push({ file: rel, prong: 'dynamic-import', detail: 'createRequire()' });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const aliases = classAliases(f, conduits, fileSet);
|
const aliases = classAliases(f, conduits, fileSet);
|
||||||
|
|
||||||
// Prong (i): schema-symbol writes — alias-, namespace-, and conduit-aware.
|
// Prong (i): schema-symbol writes — alias-, namespace-, and conduit-aware.
|
||||||
if (!inAllowlist) {
|
if (!inAllowlist) {
|
||||||
const targets: string[] = [...aliases.named];
|
// Word boundaries live inside each alternative: the bracket form ends in
|
||||||
|
// `]` (non-word), where a trailing `\b` could never match.
|
||||||
|
const targets: string[] = [...aliases.named].map((a) => `${a}\\b`);
|
||||||
|
const symAlt = CLASS_SYMBOLS.join('|');
|
||||||
for (const ns of aliases.namespaces) {
|
for (const ns of aliases.namespaces) {
|
||||||
// Allow intermediate property segments: ns.schema.companies (nested
|
// Allow intermediate property segments (ns.schema.companies — nested
|
||||||
// namespace re-exports) as well as ns.companies.
|
// namespace re-exports) and literal computed access (ns['companies']).
|
||||||
for (const s of CLASS_SYMBOLS) targets.push(`${ns}(?:\\s*\\.\\s*\\w+)*\\s*\\.\\s*${s}`);
|
targets.push(`${ns}(?:\\s*\\.\\s*\\w+)*\\s*\\.\\s*(?:${symAlt})\\b`);
|
||||||
|
targets.push(`${ns}(?:\\s*\\.\\s*\\w+)*\\s*\\[\\s*['"\`](?:${symAlt})['"\`]\\s*\\]`);
|
||||||
}
|
}
|
||||||
if (targets.length > 0) {
|
if (targets.length > 0) {
|
||||||
|
// `\(\s*\(*` tolerates argument parenthesization: .insert((companies)).
|
||||||
const writeRe = new RegExp(
|
const writeRe = new RegExp(
|
||||||
`\\.\\s*(insert|update|delete)\\s*\\(\\s*(${targets.join('|')})\\b`,
|
`\\.\\s*(insert|update|delete)\\s*\\(\\s*\\(*\\s*(${targets.join('|')})`,
|
||||||
'g',
|
'g',
|
||||||
);
|
);
|
||||||
for (const m of code.matchAll(writeRe)) {
|
for (const m of code.matchAll(writeRe)) {
|
||||||
@@ -701,10 +925,10 @@ function analyzeFile(f: FileFacts, ctx: AnalysisCtx): Violation[] {
|
|||||||
if (!inAllowlist && !isSchemaDefinition) {
|
if (!inAllowlist && !isSchemaDefinition) {
|
||||||
const kw =
|
const kw =
|
||||||
'(?:insert\\s+into|update|delete\\s+from|from|join|truncate(?:\\s+table)?|alter\\s+table|drop\\s+table|references|into|copy)';
|
'(?:insert\\s+into|update|delete\\s+from|from|join|truncate(?:\\s+table)?|alter\\s+table|drop\\s+table|references|into|copy)';
|
||||||
const gap = '(?:\\s|/\\*[\\s\\S]*?\\*/)+';
|
const gap = '(?:\\s|/\\*[\\s\\S]*?\\*/|--[^\\n]*\\n)+';
|
||||||
const q = `(?:\\\\?["'\`])?`;
|
const q = `(?:\\\\?["'\`])?`;
|
||||||
const sqlAdjacentRe = new RegExp(
|
const sqlAdjacentRe = new RegExp(
|
||||||
`\\b${kw}${gap}${q}(?:\\w+\\s*\\.\\s*)?${q}(${CLASS_TABLES.join('|')})\\b`,
|
`\\b${kw}${gap}${q}(?:${q}\\w+${q}\\s*\\.\\s*)?${q}(${CLASS_TABLES.join('|')})\\b`,
|
||||||
'i',
|
'i',
|
||||||
);
|
);
|
||||||
for (const span of f.spans) {
|
for (const span of f.spans) {
|
||||||
@@ -733,20 +957,22 @@ function analyzeFile(f: FileFacts, ctx: AnalysisCtx): Violation[] {
|
|||||||
if (r !== null && factoryConduits.has(r)) factoryConduitImport = true;
|
if (r !== null && factoryConduits.has(r)) factoryConduitImport = true;
|
||||||
}
|
}
|
||||||
const driverImport = literalDriver || conduitDriver;
|
const driverImport = literalDriver || conduitDriver;
|
||||||
const usesFactorySymbol = /\b(createDb|createPgliteDb)\b/.test(code);
|
// Factory capability: a named/namespace edge to the factory symbols, OR
|
||||||
|
// ANY value import from a factory conduit — the conduit may re-export
|
||||||
|
// the factory under a different name (export { createDb as default }),
|
||||||
|
// so the consumer-side name gate is dropped for conduit imports.
|
||||||
const factoryImport =
|
const factoryImport =
|
||||||
|
factoryConduitImport ||
|
||||||
hasSymbolImportEdge(code, DB_FACTORY_SYMBOLS, '@mosaicstack/db') ||
|
hasSymbolImportEdge(code, DB_FACTORY_SYMBOLS, '@mosaicstack/db') ||
|
||||||
(factoryConduitImport && usesFactorySymbol) ||
|
|
||||||
(aliases.namespaces.length > 0 &&
|
(aliases.namespaces.length > 0 &&
|
||||||
usesFactorySymbol &&
|
|
||||||
new RegExp(
|
new RegExp(
|
||||||
`\\b(${aliases.namespaces.join('|')})(?:\\s*\\.\\s*\\w+)*\\s*\\.\\s*(createDb|createPgliteDb)\\b`,
|
`\\b(${aliases.namespaces.join('|')})(?:\\s*\\.\\s*\\w+)*\\s*\\.\\s*(createDb|createPgliteDb)\\b`,
|
||||||
).test(code));
|
).test(code));
|
||||||
if (driverImport) {
|
if (driverImport) {
|
||||||
violations.push({ file: rel, prong: 'iii-raw-execution', detail: 'direct driver import' });
|
violations.push({ file: rel, prong: 'iii-raw-execution', detail: 'direct driver import' });
|
||||||
// A driver client exposes query/execute/unsafe as raw primitives:
|
// A driver client exposes query/execute/unsafe as raw primitives:
|
||||||
// flag them all on any receiver, any argument.
|
// flag them all on any receiver, any argument, spaced or not.
|
||||||
for (const m of code.matchAll(/\.(execute|query|unsafe)\s*\(/g)) {
|
for (const m of code.matchAll(/\.\s*(execute|query|unsafe)\s*\(/g)) {
|
||||||
violations.push({
|
violations.push({
|
||||||
file: rel,
|
file: rel,
|
||||||
prong: 'iii-raw-execution',
|
prong: 'iii-raw-execution',
|
||||||
@@ -758,7 +984,7 @@ function analyzeFile(f: FileFacts, ctx: AnalysisCtx): Violation[] {
|
|||||||
// the relational builder, and unrelated .query() methods are common),
|
// the relational builder, and unrelated .query() methods are common),
|
||||||
// so factory capability flags execute/unsafe on any receiver and query
|
// so factory capability flags execute/unsafe on any receiver and query
|
||||||
// only on the db/client-shaped receiver backstop below.
|
// only on the db/client-shaped receiver backstop below.
|
||||||
for (const m of code.matchAll(/\.(execute|unsafe)\s*\(/g)) {
|
for (const m of code.matchAll(/\.\s*(execute|unsafe)\s*\(/g)) {
|
||||||
violations.push({
|
violations.push({
|
||||||
file: rel,
|
file: rel,
|
||||||
prong: 'iii-raw-execution',
|
prong: 'iii-raw-execution',
|
||||||
@@ -770,7 +996,7 @@ function analyzeFile(f: FileFacts, ctx: AnalysisCtx): Violation[] {
|
|||||||
// DI residual backstop: db/client-shaped receivers fire regardless of
|
// DI residual backstop: db/client-shaped receivers fire regardless of
|
||||||
// detected capability (a handle can arrive by injection).
|
// detected capability (a handle can arrive by injection).
|
||||||
for (const m of code.matchAll(
|
for (const m of code.matchAll(
|
||||||
/\b(?:db|database|client|conn|connection|pool|pg|pglite|tx|trx)\.(execute|query|unsafe)\s*\(/g,
|
/\b(?:db|database|client|conn|connection|pool|pg|pglite|tx|trx)\s*\.\s*(execute|query|unsafe)\s*\(/g,
|
||||||
)) {
|
)) {
|
||||||
violations.push({
|
violations.push({
|
||||||
file: rel,
|
file: rel,
|
||||||
@@ -793,10 +1019,10 @@ function analyzeFile(f: FileFacts, ctx: AnalysisCtx): Violation[] {
|
|||||||
else if (asMatch) sqlAliases.add(asMatch[1]!);
|
else if (asMatch) sqlAliases.add(asMatch[1]!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (nsName) sqlAliases.add(`${nsName}\\.sql`);
|
if (nsName) sqlAliases.add(`${nsName}\\s*\\.\\s*sql`);
|
||||||
}
|
}
|
||||||
if (sqlAliases.size > 0) {
|
if (sqlAliases.size > 0) {
|
||||||
const rawRe = new RegExp(`\\b(${[...sqlAliases].join('|')})\\.raw\\s*\\(`);
|
const rawRe = new RegExp(`\\b(${[...sqlAliases].join('|')})\\s*\\.\\s*raw\\s*\\(`);
|
||||||
if (rawRe.test(code)) {
|
if (rawRe.test(code)) {
|
||||||
violations.push({ file: rel, prong: 'iii-raw-execution', detail: 'sql.raw()' });
|
violations.push({ file: rel, prong: 'iii-raw-execution', detail: 'sql.raw()' });
|
||||||
}
|
}
|
||||||
@@ -1035,6 +1261,101 @@ describe('hierarchy writer coverage (contract 1 §6.3b)', () => {
|
|||||||
name: 'E23 escaped-backtick-quoted table in SQL span',
|
name: 'E23 escaped-backtick-quoted table in SQL span',
|
||||||
src: 'export const q = `DELETE FROM \\`hierarchy_grants\\` WHERE role = 1`;',
|
src: 'export const q = `DELETE FROM \\`hierarchy_grants\\` WHERE role = 1`;',
|
||||||
},
|
},
|
||||||
|
// --- round-3 review shapes (G1–G6) ---
|
||||||
|
{
|
||||||
|
name: 'E24 export-site rename through conduit',
|
||||||
|
src: `import { c } from './evasion-mid4.js';\nimport { db } from './x.js';\nexport async function f() { await db.insert(c).values({}); }`,
|
||||||
|
extras: [
|
||||||
|
{
|
||||||
|
rel: 'packages/db/src/evasion-mid4.ts',
|
||||||
|
src: `export { companies as c } from '@mosaicstack/db';`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'E25 export of locally bound alias, renamed',
|
||||||
|
src: `import { co } from './evasion-mid5.js';\nimport { db } from './x.js';\nexport async function f() { await db.insert(co).values({}); }`,
|
||||||
|
extras: [
|
||||||
|
{
|
||||||
|
rel: 'packages/db/src/evasion-mid5.ts',
|
||||||
|
src: `import { companies } from '@mosaicstack/db';\nexport { companies as co };`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'E26 alias-export helper over dynamic namespace',
|
||||||
|
src: `import { co } from './evasion-mid6.js';\nimport { db } from './x.js';\nexport async function f() { await db.insert(co).values({}); }`,
|
||||||
|
extras: [
|
||||||
|
{
|
||||||
|
rel: 'packages/db/src/evasion-mid6.ts',
|
||||||
|
src: `const mod = await import('@mosaicstack/db');\nexport const co = mod.companies;`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'E27 single-const alias from namespace',
|
||||||
|
src: `import * as ns from '@mosaicstack/db';\nimport { db } from './x.js';\nconst t = ns.companies;\nexport async function f() { await db.insert(t).values({}); }`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'E28 non-class destructure from namespace (fail-closed)',
|
||||||
|
src: `import * as ns from '@mosaicstack/db';\nimport { db } from './x.js';\nconst { schema } = ns;\nexport async function f() { await db.insert(schema.companies).values({}); }`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'E29 destructure with default value',
|
||||||
|
src: `import * as ns from '@mosaicstack/db';\nimport { db } from './x.js';\nconst { companies: co = null } = ns;\nexport async function f() { await db.insert(co).values({}); }`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'E30 factory renamed to default through conduit',
|
||||||
|
src: `import createDbNow from './evasion-mid7.js';\nexport async function f(t: string) { await createDbNow('u').execute('DELETE FROM ' + t); }`,
|
||||||
|
extras: [
|
||||||
|
{
|
||||||
|
rel: 'packages/db/src/evasion-mid7.ts',
|
||||||
|
src: `export { createDb as default } from '@mosaicstack/db';`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'E31 factory alias-export helper over dynamic namespace',
|
||||||
|
src: `import { mk } from './evasion-mid8.js';\nexport async function f(t: string) { await mk('u').execute('DELETE FROM ' + t); }`,
|
||||||
|
extras: [
|
||||||
|
{
|
||||||
|
rel: 'packages/db/src/evasion-mid8.ts',
|
||||||
|
src: `const mod = await import('@mosaicstack/db');\nexport const mk = mod.createDb;`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'E32 createRequire import alias',
|
||||||
|
src: `import { createRequire as mkReq } from 'node:module';\nconst req = mkReq(import.meta.url);\nexport const pg = req('postgres');`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'E33 then-form dynamic schema import',
|
||||||
|
src: `import { db } from './x.js';\nexport function f() { return import('@mosaicstack/db').then((m) => db.insert(m.companies).values({})); }`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'E34 quoted schema qualifier in SQL span',
|
||||||
|
src: `export const q = 'DELETE FROM "public".hierarchy_grants WHERE role = $1';`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'E35 SQL line comment interposed in SQL span',
|
||||||
|
src: 'export const q = `DELETE FROM -- audit\nhierarchy_grants`;',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'E36 spaced member access on conventional receiver',
|
||||||
|
src: `export class R { constructor(private db: { query(s: string): Promise<unknown> }) {}\n async f(t: string) { await this.db . query ('TRUNCATE ' + t); } }`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'E37 bracket-form schema argument',
|
||||||
|
src: `import * as ns from '@mosaicstack/db';\nimport { db } from './x.js';\nexport async function f() { await db.insert(ns['companies']).values({}); }`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'E38 parenthesized schema argument',
|
||||||
|
src: `import { companies } from '@mosaicstack/db';\nimport { db } from './x.js';\nexport async function f() { await db.insert((companies)).values({}); }`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'E39 computed-member eval',
|
||||||
|
src: `export function f(s: string) { return (globalThis as never)['eval'](s); }`,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
const CLEAN_CONTROLS: Array<{ name: string; src: string }> = [
|
const CLEAN_CONTROLS: Array<{ name: string; src: string }> = [
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user