fix(db): close round-4 review evasions in writer-coverage assertion
ci/woodpecker/pr/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful
Default-export slot treated as an export name at both ends (helper-side export-default passes, consumer-side bare-default-import resolution); namespace-member write targets match the source's exported names via a per-file memberSyms union; factory conduits carry a name map computed to a fixpoint so renames propagate hop by hop (export-from, export-of-local, export default); tracked dynamic imports of capability conduits confer capability; factory destructure-rename from a schema namespace; static module-namespace createRequire destructure; spread-argument writes; DCL keywords (GRANT/REVOKE ON, LOCK TABLE); bracketed conventional DI receiver. Permanent controls E40-E53.
This commit is contained in:
@@ -15,11 +15,17 @@
|
|||||||
* (`export { companies as c } from`), an export of a locally bound
|
* (`export { companies as c } from`), an export of a locally bound
|
||||||
* alias (`import { companies }; export { companies as co }`), and a
|
* alias (`import { companies }; export { companies as co }`), and a
|
||||||
* binding derived from a namespace (`export const co = ns.companies`)
|
* binding derived from a namespace (`export const co = ns.companies`)
|
||||||
* all propagate symbol identity to the consumer. Destructuring a
|
* all propagate symbol identity to the consumer. The DEFAULT-export
|
||||||
* NON-class property from a schema namespace over-approximates: the
|
* slot is an export name like any other (`export { companies as
|
||||||
* binding is treated as a schema namespace itself (fail-closed), and
|
* default } from`, `export default companies`), so a bare default
|
||||||
* single-alias derivation from a namespace (`const t = ns.companies`)
|
* import of a conduit resolves against the map too; namespace-member
|
||||||
* is folded in to a fixed depth.
|
* write targets match the source's EXPORTED names — renames included
|
||||||
|
* (`M.co` after `export { companies as co } from`) — not just the
|
||||||
|
* original class symbols. 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
|
||||||
@@ -27,7 +33,13 @@
|
|||||||
* migrations are excluded from this prong only (per contract).
|
* migrations are excluded from this prong only (per contract).
|
||||||
* (iii) Raw-execution prong — content-independent. A file is RAW-CAPABLE
|
* (iii) Raw-execution prong — content-independent. A file is RAW-CAPABLE
|
||||||
* when it imports a database driver or the createDb/createPgliteDb
|
* when it imports a database driver or the createDb/createPgliteDb
|
||||||
* factories (directly or via namespace). In a raw-capable file
|
* factories — directly, via namespace, by destructuring a factory
|
||||||
|
* symbol out of a schema namespace, or from a capability CONDUIT.
|
||||||
|
* Conduits carry their own exported-name maps computed to a
|
||||||
|
* fixpoint, so a factory renamed at any hop (`export { createDb as
|
||||||
|
* mk } from`, `export default createDb`) still marks every module
|
||||||
|
* down the chain, and a tracked dynamic import of a conduit is an
|
||||||
|
* import edge like a static one. In a raw-capable file
|
||||||
* outside the allowlist and register, EVERY `.execute(`, `.query(`,
|
* outside the allowlist and register, EVERY `.execute(`, `.query(`,
|
||||||
* and `.unsafe(` call fails, on any receiver, with any argument —
|
* and `.unsafe(` call fails, on any receiver, with any argument —
|
||||||
* there is no tagged-template exemption (§6.3(b): "regardless of what
|
* there is no tagged-template exemption (§6.3(b): "regardless of what
|
||||||
@@ -43,7 +55,9 @@
|
|||||||
* Unanalyzable import routes fail everywhere except their own enumerated
|
* Unanalyzable import routes fail everywhere except their own enumerated
|
||||||
* disposition registers below: a dynamic import whose specifier is not a
|
* disposition registers below: a dynamic import whose specifier is not a
|
||||||
* single string literal, and createRequire (tracked through import aliasing
|
* single string literal, and createRequire (tracked through import aliasing
|
||||||
* — `createRequire as x` — and destructuring from the module namespace; a
|
* — `createRequire as x` — and destructuring from the module namespace,
|
||||||
|
* whether that namespace came from a static `import * as` or a tracked
|
||||||
|
* dynamic import; a
|
||||||
* route that hides the NAME entirely, e.g. re-exporting createRequire from 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
|
* helper, falls under the value-flow residual). A LITERAL dynamic import is
|
||||||
* an import edge like any static import (contract rev 9): its bindings
|
* an import edge like any static import (contract rev 9): its bindings
|
||||||
@@ -543,6 +557,21 @@ function computeSchemaConduits(files: FileFacts[], fileSet: Set<string>): Schema
|
|||||||
mine.named.add(m[1]!);
|
mine.named.add(m[1]!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// The default slot is an export name like any other:
|
||||||
|
// `export default companies;` / `export default mod.companies;`
|
||||||
|
for (const m of f.code.matchAll(/export\s+default\s+(\w+)\s*;/g)) {
|
||||||
|
if (aliases.named.includes(m[1]!)) mine.named.add('default');
|
||||||
|
if (aliases.namespaces.includes(m[1]!)) mine.ns.add('default');
|
||||||
|
}
|
||||||
|
for (const ns of aliases.namespaces) {
|
||||||
|
if (
|
||||||
|
new RegExp(
|
||||||
|
`export\\s+default\\s+${ns}(?:\\s*\\.\\s*\\w+)*\\s*\\.\\s*(?:${CLASS_SYMBOLS.join('|')})\\b`,
|
||||||
|
).test(f.code)
|
||||||
|
) {
|
||||||
|
mine.named.add('default');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (mine.named.size + mine.ns.size > before) {
|
if (mine.named.size + mine.ns.size > before) {
|
||||||
conduits.set(f.rel, mine);
|
conduits.set(f.rel, mine);
|
||||||
@@ -556,6 +585,10 @@ function computeSchemaConduits(files: FileFacts[], fileSet: Set<string>): Schema
|
|||||||
interface ClassAliases {
|
interface ClassAliases {
|
||||||
named: string[]; // local identifiers bound to class-table symbols
|
named: string[]; // local identifiers bound to class-table symbols
|
||||||
namespaces: string[]; // namespace identifiers over a schema source
|
namespaces: string[]; // namespace identifiers over a schema source
|
||||||
|
// Member names under which a class symbol is reachable on SOME imported
|
||||||
|
// schema source (class symbols plus every renamed conduit export the file
|
||||||
|
// imports) — the alternation for namespace-member write targets.
|
||||||
|
memberSyms: Set<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -605,13 +638,21 @@ function destructureBindings(
|
|||||||
function classAliases(f: FileFacts, conduits: SchemaConduits, fileSet: Set<string>): ClassAliases {
|
function classAliases(f: FileFacts, conduits: SchemaConduits, fileSet: Set<string>): ClassAliases {
|
||||||
const named = new Set<string>();
|
const named = new Set<string>();
|
||||||
const namespaces = new Set<string>();
|
const namespaces = new Set<string>();
|
||||||
|
const memberSyms = new Set<string>(CLASS_SYMBOLS);
|
||||||
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, defaultWith, namedList, nsName, defaultBare, spec] = m;
|
||||||
if (typeOnly) continue;
|
if (typeOnly) continue;
|
||||||
const src = schemaExportsOf(f.rel, spec!, conduits, fileSet);
|
const src = schemaExportsOf(f.rel, spec!, conduits, fileSet);
|
||||||
if (src === null || (src.named.size === 0 && src.ns.size === 0)) continue;
|
if (src === null || (src.named.size === 0 && src.ns.size === 0)) continue;
|
||||||
|
for (const n of src.named) memberSyms.add(n);
|
||||||
if (nsName) namespaces.add(nsName);
|
if (nsName) namespaces.add(nsName);
|
||||||
if (namedList) importBindings(namedList, src, named, namespaces);
|
if (namedList) importBindings(namedList, src, named, namespaces);
|
||||||
|
// A default import binds whatever the source exports as `default`.
|
||||||
|
const dflt = defaultWith ?? defaultBare;
|
||||||
|
if (dflt) {
|
||||||
|
if (src.named.has('default')) named.add(dflt);
|
||||||
|
if (src.ns.has('default')) namespaces.add(dflt);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// 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).
|
||||||
@@ -621,6 +662,7 @@ function classAliases(f: FileFacts, conduits: SchemaConduits, fileSet: Set<strin
|
|||||||
const [, pattern, nsName, , spec] = m;
|
const [, pattern, nsName, , spec] = m;
|
||||||
const src = schemaExportsOf(f.rel, spec!, conduits, fileSet);
|
const src = schemaExportsOf(f.rel, spec!, conduits, fileSet);
|
||||||
if (src === null || (src.named.size === 0 && src.ns.size === 0)) continue;
|
if (src === null || (src.named.size === 0 && src.ns.size === 0)) continue;
|
||||||
|
for (const n of src.named) memberSyms.add(n);
|
||||||
if (nsName) namespaces.add(nsName);
|
if (nsName) namespaces.add(nsName);
|
||||||
if (pattern) destructureBindings(pattern, src, named, namespaces);
|
if (pattern) destructureBindings(pattern, src, named, namespaces);
|
||||||
}
|
}
|
||||||
@@ -629,7 +671,7 @@ function classAliases(f: FileFacts, conduits: SchemaConduits, fileSet: Set<strin
|
|||||||
// const { schema } = ns; → nested namespace (fail-closed)
|
// const { schema } = ns; → nested namespace (fail-closed)
|
||||||
// const t = ns.companies; → named alias
|
// const t = ns.companies; → named alias
|
||||||
// const s2 = ns.schema; → namespace alias
|
// const s2 = ns.schema; → namespace alias
|
||||||
const nsSrc: SchemaExports = { named: new Set(CLASS_SYMBOLS), ns: new Set() };
|
const nsSrc: SchemaExports = { named: memberSyms, ns: new Set() };
|
||||||
for (let pass = 0; pass < 2; pass += 1) {
|
for (let pass = 0; pass < 2; pass += 1) {
|
||||||
for (const ns of [...namespaces]) {
|
for (const ns of [...namespaces]) {
|
||||||
for (const m of f.code.matchAll(
|
for (const m of f.code.matchAll(
|
||||||
@@ -643,12 +685,12 @@ function classAliases(f: FileFacts, conduits: SchemaConduits, fileSet: Set<strin
|
|||||||
'g',
|
'g',
|
||||||
),
|
),
|
||||||
)) {
|
)) {
|
||||||
if (CLASS_SYMBOLS.includes(m[2]!)) named.add(m[1]!);
|
if (memberSyms.has(m[2]!)) named.add(m[1]!);
|
||||||
else namespaces.add(m[1]!);
|
else namespaces.add(m[1]!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { named: [...named], namespaces: [...namespaces] };
|
return { named: [...named], namespaces: [...namespaces], memberSyms };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -658,27 +700,34 @@ function classAliases(f: FileFacts, conduits: SchemaConduits, fileSet: Set<strin
|
|||||||
* another driver conduit; importing ANYTHING from one confers driver
|
* another driver conduit; importing ANYTHING from one confers driver
|
||||||
* 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` — carries a NAME MAP (module → the exported names under which a
|
||||||
* another factory conduit (via `export … from`, or by exporting a local
|
* factory is reachable), computed to a fixpoint: `export … from` propagates
|
||||||
* binding of the factory under any name); importing ANYTHING from one
|
* the SOURCE's exported factory names through renames (star and star-as
|
||||||
* confers factory capability — the conduit may rename the symbol, so there
|
* copy them all), and a local binding of a factory (named import, namespace
|
||||||
* is no consumer-side name gate.
|
* member extraction, or tracked dynamic-import destructure) exported under
|
||||||
|
* any name — braces, declaration, or `export default` — adds that name.
|
||||||
|
* Importing ANYTHING from a factory conduit (statically or via a tracked
|
||||||
|
* dynamic import) confers factory capability — the conduit may rename the
|
||||||
|
* symbol at any hop, so there is no consumer-side name gate.
|
||||||
*/
|
*/
|
||||||
function computeCapabilityConduits(
|
function computeCapabilityConduits(
|
||||||
files: FileFacts[],
|
files: FileFacts[],
|
||||||
fileSet: Set<string>,
|
fileSet: Set<string>,
|
||||||
): { driver: Set<string>; factory: Set<string> } {
|
): { driver: Set<string>; factory: Set<string> } {
|
||||||
const driver = new Set<string>();
|
const driver = new Set<string>();
|
||||||
const factory = new Set<string>();
|
// Factory conduits carry a NAME MAP (module → exported names under which a
|
||||||
|
// factory is reachable) so renames propagate hop by hop, exactly like the
|
||||||
|
// schema export map — a literal-name gate at any hop would launder.
|
||||||
|
const factoryNames = new Map<string, Set<string>>();
|
||||||
const isDriverSpec = (rel: string, spec: string): boolean => {
|
const isDriverSpec = (rel: string, spec: string): boolean => {
|
||||||
if (DRIVER_SPECIFIERS.includes(spec)) return true;
|
if (DRIVER_SPECIFIERS.includes(spec)) return true;
|
||||||
const r = resolveSpecifier(rel, spec, fileSet);
|
const r = resolveSpecifier(rel, spec, fileSet);
|
||||||
return r !== null && driver.has(r);
|
return r !== null && driver.has(r);
|
||||||
};
|
};
|
||||||
const isFactorySpec = (rel: string, spec: string): boolean => {
|
const factoryNamesOf = (rel: string, spec: string): Set<string> | null => {
|
||||||
if (spec === '@mosaicstack/db') return true;
|
if (spec === '@mosaicstack/db') return new Set(DB_FACTORY_SYMBOLS);
|
||||||
const r = resolveSpecifier(rel, spec, fileSet);
|
const r = resolveSpecifier(rel, spec, fileSet);
|
||||||
return r !== null && factory.has(r);
|
return r !== null ? (factoryNames.get(r) ?? null) : null;
|
||||||
};
|
};
|
||||||
let changed = true;
|
let changed = true;
|
||||||
while (changed) {
|
while (changed) {
|
||||||
@@ -691,31 +740,44 @@ function computeCapabilityConduits(
|
|||||||
driver.add(f.rel);
|
driver.add(f.rel);
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
if (!factory.has(f.rel) && isFactorySpec(f.rel, spec)) {
|
|
||||||
const named = m[2];
|
|
||||||
if (named === undefined || /\b(createDb|createPgliteDb)\b/.test(named)) {
|
|
||||||
factory.add(f.rel);
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// `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
|
const mine = factoryNames.get(f.rel) ?? new Set<string>();
|
||||||
// (named import, namespace member, or tracked dynamic import) and
|
const before = mine.size;
|
||||||
// exports that binding under any name is a factory conduit even with
|
for (const m of f.code.matchAll(EXPORT_FROM_RE)) {
|
||||||
// no `export … from` clause (`export const mk = mod.createDb`).
|
if (m[1]) continue;
|
||||||
if (!factory.has(f.rel)) {
|
const src = factoryNamesOf(f.rel, m[3]!);
|
||||||
|
if (src === null || src.size === 0) continue;
|
||||||
|
if (m[2] === undefined) {
|
||||||
|
// `export *` / `export * as x` — over-approximated to the source's
|
||||||
|
// names (membership is what confers capability on consumers).
|
||||||
|
for (const n of src) mine.add(n);
|
||||||
|
} else {
|
||||||
|
for (const part of m[2].split(',')) {
|
||||||
|
const seg = part.trim();
|
||||||
|
if (!seg || seg.startsWith('type ')) continue;
|
||||||
|
const am = /^(\w+)\s+as\s+(\w+)$/.exec(seg);
|
||||||
|
if (src.has(am ? am[1]! : seg)) mine.add(am ? am[2]! : seg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Locals bound to a factory (named import — aliased or not — namespace
|
||||||
|
// member extraction, or tracked dynamic-import destructure), then
|
||||||
|
// exported under ANY name and by ANY form, braces or default included
|
||||||
|
// (`export const mk = mod.createDb`, `export default createDb`).
|
||||||
const locals = new Set<string>();
|
const locals = new Set<string>();
|
||||||
const nss = new Set<string>();
|
const nss = new Map<string, Set<string>>();
|
||||||
for (const im of f.code.matchAll(IMPORT_RE)) {
|
for (const im of f.code.matchAll(IMPORT_RE)) {
|
||||||
const [, typeOnly, , namedList, nsName, , spec] = im;
|
const [, typeOnly, , namedList, nsName, , spec] = im;
|
||||||
if (typeOnly || !isFactorySpec(f.rel, spec!)) continue;
|
if (typeOnly) continue;
|
||||||
if (nsName) nss.add(nsName);
|
const src = factoryNamesOf(f.rel, spec!);
|
||||||
|
if (src === null || src.size === 0) continue;
|
||||||
|
if (nsName) nss.set(nsName, src);
|
||||||
if (namedList) {
|
if (namedList) {
|
||||||
for (const part of namedList.split(',')) {
|
for (const part of namedList.split(',')) {
|
||||||
const am = /^(createDb|createPgliteDb)(?:\s+as\s+(\w+))?$/.exec(part.trim());
|
const am = /^(\w+)(?:\s+as\s+(\w+))?$/.exec(part.trim());
|
||||||
if (am) locals.add(am[2] ?? am[1]!);
|
if (am && src.has(am[1]!)) locals.add(am[2] ?? am[1]!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -723,39 +785,48 @@ function computeCapabilityConduits(
|
|||||||
/(?: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] = dm;
|
const [, pattern, nsName, , spec] = dm;
|
||||||
if (!isFactorySpec(f.rel, spec!)) continue;
|
const src = factoryNamesOf(f.rel, spec!);
|
||||||
if (nsName) nss.add(nsName);
|
if (src === null || src.size === 0) continue;
|
||||||
|
if (nsName) nss.set(nsName, src);
|
||||||
if (pattern) {
|
if (pattern) {
|
||||||
for (const part of pattern.split(',')) {
|
for (const part of pattern.split(',')) {
|
||||||
const pm = /^(createDb|createPgliteDb)\s*(?::\s*(\w+))?/.exec(part.trim());
|
const pm = /^(\w+)\s*(?::\s*(\w+))?/.exec(part.trim());
|
||||||
if (pm) locals.add(pm[2] ?? pm[1]!);
|
if (pm && src.has(pm[1]!)) locals.add(pm[2] ?? pm[1]!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const ns of nss) {
|
for (const [ns, src] of nss) {
|
||||||
for (const am of f.code.matchAll(
|
for (const am of f.code.matchAll(
|
||||||
new RegExp(
|
new RegExp(`(?:const|let|var)\\s+(\\w+)\\s*=\\s*${ns}\\s*\\.\\s*(\\w+)\\b`, 'g'),
|
||||||
`(?:const|let|var)\\s+(\\w+)\\s*=\\s*${ns}\\s*\\.\\s*(?:createDb|createPgliteDb)\\b`,
|
|
||||||
'g',
|
|
||||||
),
|
|
||||||
)) {
|
)) {
|
||||||
locals.add(am[1]!);
|
if (src.has(am[2]!)) locals.add(am[1]!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (locals.size > 0) {
|
||||||
|
for (const m of f.code.matchAll(/export\s*\{([^}]*)\}(?!\s*from)/g)) {
|
||||||
|
for (const part of m[1]!.split(',')) {
|
||||||
|
const seg = part.trim();
|
||||||
|
if (!seg || seg.startsWith('type ')) continue;
|
||||||
|
const am = /^(\w+)(?:\s+as\s+(\w+))?$/.exec(seg);
|
||||||
|
if (am && locals.has(am[1]!)) mine.add(am[2] ?? am[1]!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const local of locals) {
|
for (const local of locals) {
|
||||||
if (
|
if (new RegExp(`export\\s+(?:const|let|var|function)\\s+${local}\\b`).test(f.code)) {
|
||||||
new RegExp(`export\\s+(?:const|let|var|function)\\s+${local}\\b`).test(f.code) ||
|
mine.add(local);
|
||||||
new RegExp(`export\\s*\\{[^}]*\\b${local}\\b[^}]*\\}`).test(f.code)
|
}
|
||||||
) {
|
if (new RegExp(`export\\s+default\\s+${local}\\b`).test(f.code)) {
|
||||||
factory.add(f.rel);
|
mine.add('default');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (mine.size > before) {
|
||||||
|
factoryNames.set(f.rel, mine);
|
||||||
changed = true;
|
changed = true;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
return { driver, factory: new Set(factoryNames.keys()) };
|
||||||
}
|
|
||||||
return { driver, factory };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Value-import edge naming one of `symbols` (named import from anywhere, or literal dynamic package import while using the symbol). */
|
/** Value-import edge naming one of `symbols` (named import from anywhere, or literal dynamic package import while using the symbol). */
|
||||||
@@ -854,6 +925,25 @@ function analyzeFile(f: FileFacts, ctx: AnalysisCtx): Violation[] {
|
|||||||
const alias = /\bcreateRequire\s*:\s*(\w+)/.exec(m[1]!);
|
const alias = /\bcreateRequire\s*:\s*(\w+)/.exec(m[1]!);
|
||||||
if (alias) crNames.add(alias[1]!);
|
if (alias) crNames.add(alias[1]!);
|
||||||
}
|
}
|
||||||
|
// Destructuring from a STATIC or dynamically-bound module namespace:
|
||||||
|
// import * as M from 'node:module'; const { createRequire: x } = M;
|
||||||
|
const modNs = new Set<string>();
|
||||||
|
for (const m of code.matchAll(/import\s*\*\s*as\s+(\w+)\s*from\s*['"](?:node:)?module['"]/g)) {
|
||||||
|
modNs.add(m[1]!);
|
||||||
|
}
|
||||||
|
for (const m of code.matchAll(
|
||||||
|
/(?:const|let|var)\s+(\w+)\s*=\s*await\s+import\s*\(\s*['"](?:node:)?module['"]\s*\)/g,
|
||||||
|
)) {
|
||||||
|
modNs.add(m[1]!);
|
||||||
|
}
|
||||||
|
for (const ns of modNs) {
|
||||||
|
for (const m of code.matchAll(
|
||||||
|
new RegExp(`(?:const|let|var)\\s*\\{([^}]*)\\}\\s*=\\s*${ns}\\b`, 'g'),
|
||||||
|
)) {
|
||||||
|
const alias = /\bcreateRequire\s*(?::\s*(\w+))?/.exec(m[1]!);
|
||||||
|
if (alias) crNames.add(alias[1] ?? 'createRequire');
|
||||||
|
}
|
||||||
|
}
|
||||||
for (const n of crNames) {
|
for (const n of crNames) {
|
||||||
if (n === 'createRequire' || new RegExp(`\\b${n}\\s*\\(`).test(code)) {
|
if (n === 'createRequire' || new RegExp(`\\b${n}\\s*\\(`).test(code)) {
|
||||||
violations.push({ file: rel, prong: 'dynamic-import', detail: 'createRequire()' });
|
violations.push({ file: rel, prong: 'dynamic-import', detail: 'createRequire()' });
|
||||||
@@ -869,7 +959,9 @@ function analyzeFile(f: FileFacts, ctx: AnalysisCtx): Violation[] {
|
|||||||
// Word boundaries live inside each alternative: the bracket form ends in
|
// Word boundaries live inside each alternative: the bracket form ends in
|
||||||
// `]` (non-word), where a trailing `\b` could never match.
|
// `]` (non-word), where a trailing `\b` could never match.
|
||||||
const targets: string[] = [...aliases.named].map((a) => `${a}\\b`);
|
const targets: string[] = [...aliases.named].map((a) => `${a}\\b`);
|
||||||
const symAlt = CLASS_SYMBOLS.join('|');
|
// Namespace members match the source's EXPORTED names (renames included),
|
||||||
|
// not just the original class symbols.
|
||||||
|
const symAlt = [...aliases.memberSyms].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) and literal computed access (ns['companies']).
|
// namespace re-exports) and literal computed access (ns['companies']).
|
||||||
@@ -877,9 +969,10 @@ function analyzeFile(f: FileFacts, ctx: AnalysisCtx): Violation[] {
|
|||||||
targets.push(`${ns}(?:\\s*\\.\\s*\\w+)*\\s*\\[\\s*['"\`](?:${symAlt})['"\`]\\s*\\]`);
|
targets.push(`${ns}(?:\\s*\\.\\s*\\w+)*\\s*\\[\\s*['"\`](?:${symAlt})['"\`]\\s*\\]`);
|
||||||
}
|
}
|
||||||
if (targets.length > 0) {
|
if (targets.length > 0) {
|
||||||
// `\(\s*\(*` tolerates argument parenthesization: .insert((companies)).
|
// The argument prefix tolerates parenthesization, spread, and array
|
||||||
|
// wrapping: .insert((companies)), .insert(...[companies]).
|
||||||
const writeRe = new RegExp(
|
const writeRe = new RegExp(
|
||||||
`\\.\\s*(insert|update|delete)\\s*\\(\\s*\\(*\\s*(${targets.join('|')})`,
|
`\\.\\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)) {
|
||||||
@@ -924,7 +1017,7 @@ function analyzeFile(f: FileFacts, ctx: AnalysisCtx): Violation[] {
|
|||||||
// block comments, and quoting (including escaped quotes in span text).
|
// block comments, and quoting (including escaped quotes in span text).
|
||||||
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|on|lock(?:\\s+table)?)';
|
||||||
const gap = '(?:\\s|/\\*[\\s\\S]*?\\*/|--[^\\n]*\\n)+';
|
const gap = '(?:\\s|/\\*[\\s\\S]*?\\*/|--[^\\n]*\\n)+';
|
||||||
const q = `(?:\\\\?["'\`])?`;
|
const q = `(?:\\\\?["'\`])?`;
|
||||||
const sqlAdjacentRe = new RegExp(
|
const sqlAdjacentRe = new RegExp(
|
||||||
@@ -956,6 +1049,13 @@ function analyzeFile(f: FileFacts, ctx: AnalysisCtx): Violation[] {
|
|||||||
if (r !== null && driverConduits.has(r)) conduitDriver = true;
|
if (r !== null && driverConduits.has(r)) conduitDriver = true;
|
||||||
if (r !== null && factoryConduits.has(r)) factoryConduitImport = true;
|
if (r !== null && factoryConduits.has(r)) factoryConduitImport = true;
|
||||||
}
|
}
|
||||||
|
// A TRACKED dynamic import of a conduit is an import edge like any
|
||||||
|
// static one (contract rev 9) — for capability too, not just prong (i).
|
||||||
|
for (const m of f.code.matchAll(/=\s*await\s+import\s*\(\s*(['"])([^'"]+)\1\s*\)/g)) {
|
||||||
|
const r = resolveSpecifier(rel, m[2]!, fileSet);
|
||||||
|
if (r !== null && driverConduits.has(r)) conduitDriver = true;
|
||||||
|
if (r !== null && factoryConduits.has(r)) factoryConduitImport = true;
|
||||||
|
}
|
||||||
const driverImport = literalDriver || conduitDriver;
|
const driverImport = literalDriver || conduitDriver;
|
||||||
// Factory capability: a named/namespace edge to the factory symbols, OR
|
// Factory capability: a named/namespace edge to the factory symbols, OR
|
||||||
// ANY value import from a factory conduit — the conduit may re-export
|
// ANY value import from a factory conduit — the conduit may re-export
|
||||||
@@ -967,6 +1067,12 @@ function analyzeFile(f: FileFacts, ctx: AnalysisCtx): Violation[] {
|
|||||||
(aliases.namespaces.length > 0 &&
|
(aliases.namespaces.length > 0 &&
|
||||||
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)) ||
|
||||||
|
// Destructuring a factory symbol OUT of a schema namespace confers
|
||||||
|
// capability whatever the local rename: const { createDb: mk } = dbns.
|
||||||
|
(aliases.namespaces.length > 0 &&
|
||||||
|
new RegExp(
|
||||||
|
`(?:const|let|var)\\s*\\{[^}]*\\b(?:createDb|createPgliteDb)\\b[^}]*\\}\\s*=\\s*(?:await\\s+)?(?:${aliases.namespaces.join('|')})\\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' });
|
||||||
@@ -995,8 +1101,10 @@ function analyzeFile(f: FileFacts, ctx: AnalysisCtx): Violation[] {
|
|||||||
if (!driverImport) {
|
if (!driverImport) {
|
||||||
// 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).
|
||||||
|
// The receiver may be a dotted name OR a literal bracketed member with
|
||||||
|
// a conventional name: this['db'].query(…).
|
||||||
for (const m of code.matchAll(
|
for (const m of code.matchAll(
|
||||||
/\b(?:db|database|client|conn|connection|pool|pg|pglite|tx|trx)\s*\.\s*(execute|query|unsafe)\s*\(/g,
|
/(?:\b(?:db|database|client|conn|connection|pool|pg|pglite|tx|trx)|\[\s*['"](?:db|database|client|conn|connection|pool|pg|pglite|tx|trx)['"]\s*\])\s*\.\s*(execute|query|unsafe)\s*\(/g,
|
||||||
)) {
|
)) {
|
||||||
violations.push({
|
violations.push({
|
||||||
file: rel,
|
file: rel,
|
||||||
@@ -1356,6 +1464,114 @@ describe('hierarchy writer coverage (contract 1 §6.3b)', () => {
|
|||||||
name: 'E39 computed-member eval',
|
name: 'E39 computed-member eval',
|
||||||
src: `export function f(s: string) { return (globalThis as never)['eval'](s); }`,
|
src: `export function f(s: string) { return (globalThis as never)['eval'](s); }`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'E40 schema symbol renamed to default through conduit',
|
||||||
|
src: `import c from './evasion-mid9.js';\nimport { db } from './x.js';\nexport async function f() { await db.insert(c).values({}); }`,
|
||||||
|
extras: [
|
||||||
|
{
|
||||||
|
rel: 'packages/db/src/evasion-mid9.ts',
|
||||||
|
src: `export { companies as default } from '@mosaicstack/db';`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'E41 export default of locally bound schema symbol',
|
||||||
|
src: `import c from './evasion-mid10.js';\nimport { db } from './x.js';\nexport async function f() { await db.insert(c).values({}); }`,
|
||||||
|
extras: [
|
||||||
|
{
|
||||||
|
rel: 'packages/db/src/evasion-mid10.ts',
|
||||||
|
src: `import { companies } from '@mosaicstack/db';\nexport default companies;`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'E42 export default of dynamic namespace member',
|
||||||
|
src: `import c from './evasion-mid11.js';\nimport { db } from './x.js';\nexport async function f() { await db.insert(c).values({}); }`,
|
||||||
|
extras: [
|
||||||
|
{
|
||||||
|
rel: 'packages/db/src/evasion-mid11.ts',
|
||||||
|
src: `const mod = await import('@mosaicstack/db');\nexport default mod.companies;`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'E43 namespace member under renamed export',
|
||||||
|
src: `import * as M from './evasion-mid12.js';\nimport { db } from './x.js';\nexport async function f() { await db.insert(M.co).values({}); }`,
|
||||||
|
extras: [
|
||||||
|
{
|
||||||
|
rel: 'packages/db/src/evasion-mid12.ts',
|
||||||
|
src: `export { companies as co } from '@mosaicstack/db';`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'E44 dynamic namespace member under renamed export',
|
||||||
|
src: `import { db } from './x.js';\nconst M = await import('./evasion-mid13.js');\nexport async function f() { await db.insert(M.co).values({}); }`,
|
||||||
|
extras: [
|
||||||
|
{
|
||||||
|
rel: 'packages/db/src/evasion-mid13.ts',
|
||||||
|
src: `export { companies as co } from '@mosaicstack/db';`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'E45 two-hop factory export-from rename',
|
||||||
|
src: `import { mk } from './evasion-mid15.js';\nexport async function f(t: string) { await mk('u').execute('DELETE FROM ' + t); }`,
|
||||||
|
extras: [
|
||||||
|
{
|
||||||
|
rel: 'packages/db/src/evasion-mid14.ts',
|
||||||
|
src: `export { createDb as mk } from '@mosaicstack/db';`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rel: 'packages/db/src/evasion-mid15.ts',
|
||||||
|
src: `export { mk } from './evasion-mid14.js';`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'E46 export default of locally bound factory',
|
||||||
|
src: `import mk from './evasion-mid16.js';\nexport async function f(t: string) { await mk('u').execute('DELETE FROM ' + t); }`,
|
||||||
|
extras: [
|
||||||
|
{
|
||||||
|
rel: 'packages/db/src/evasion-mid16.ts',
|
||||||
|
src: `import { createDb } from '@mosaicstack/db';\nexport default createDb;`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'E47 tracked dynamic import of a factory conduit',
|
||||||
|
src: `const F = await import('./evasion-mid17.js');\nexport async function f(t: string) { await F.mk('u').execute('DELETE FROM ' + t); }`,
|
||||||
|
extras: [
|
||||||
|
{
|
||||||
|
rel: 'packages/db/src/evasion-mid17.ts',
|
||||||
|
src: `export { createDb as mk } from '@mosaicstack/db';`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'E48 factory destructure-rename from schema namespace',
|
||||||
|
src: `import * as dbns from '@mosaicstack/db';\nconst { createDb: mk } = dbns;\nexport async function f(t: string) { await mk('u').execute('DELETE FROM ' + t); }`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'E49 createRequire destructured from static module namespace',
|
||||||
|
src: `import * as M from 'node:module';\nconst { createRequire: mkReq } = M;\nconst req = mkReq(import.meta.url);\nexport const pg = req('postgres');`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'E50 spread-argument schema write',
|
||||||
|
src: `import { companies } from '@mosaicstack/db';\nimport { db } from './x.js';\nexport async function f() { await db.insert(...[companies]).values({}); }`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'E51 GRANT ON class table in SQL span',
|
||||||
|
src: `export const q = 'GRANT SELECT ON hierarchy_grants TO auditor';`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'E52 LOCK TABLE class table in SQL span',
|
||||||
|
src: `export const q = 'LOCK TABLE hierarchy_grants IN ACCESS EXCLUSIVE MODE';`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'E53 bracketed conventional receiver',
|
||||||
|
src: `export class R { async f(t: string) { await this['db'].query('TRUNCATE ' + t); } }`,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
const CLEAN_CONTROLS: Array<{ name: string; src: string }> = [
|
const CLEAN_CONTROLS: Array<{ name: string; src: string }> = [
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user