diff --git a/docs/memorys/2026-06-02-S1Task9最终验证门禁.md b/docs/memorys/2026-06-02-S1Task9最终验证门禁.md index 868baa81..cb7bdcc3 100644 --- a/docs/memorys/2026-06-02-S1Task9最终验证门禁.md +++ b/docs/memorys/2026-06-02-S1Task9最终验证门禁.md @@ -1,89 +1,74 @@ -# 2026-06-02 S1 Task 9 最终验证门禁 +# 2026-06-02 S1 Task9 最终验证门禁 ## 结论 -S1 Task 9 `Final S1 Verification` 已完成,并通过 mandatory fresh review gate 与 S1 阶段最终 read-only review gate: +S1 Task9 `Final S1 Verification` 本轮已通过: -- Task 9 scope scanner spec compliance review:最终 PASS,无 Critical / Important。 -- Task 9 scope scanner quality / feasibility review:最终 PASS,无 Critical / Important。 -- Task 9 full verification:PASS。 -- S1 final read-only review:PASS,无 Critical / Important,可以进入 S2。 +- Scope gate 实现链:DONE。 +- Fresh spec review gate:PASS,无 Critical / Important blocker。 +- Fresh quality / feasibility review gate:PASS,无 Critical / Important blocker。 +- Full Task9 verification:PASS。 -## 实现范围 - -Task 9 delta 包含: +S1 Task9 实际修改范围: - `scripts/check-s1-scope.mjs` -- `apps/api/src/modules/jobs/index.ts` +- `docs/memorys/2026-06-02-S1Task9最终验证门禁.md` -Task 9 未实现: +当前准备提交范围还包含两个 S2 handoff 文档;它们来自既有 dirty baseline,本轮只在整理提交阶段按 review feedback 做文档自洽修正: -- S2+ creation agent / 游戏需求确认 / draft 生成 / draft 修改。 -- Web runtime、WebPlatformAdapter、小游戏转换、发布流、feed、analytics。 -- 新业务 API、schema model 或 migration。 +- `docs/superpowers/plans/2026-05-31-mvp-S2-creator-workbench.md` +- `docs/superpowers/specs/2026-05-31-mvp-S2-creator-workbench-design.md` -## 关键实现事实 +## 子代理与 Review Gate -- `scripts/check-s1-scope.mjs` 扫描 `apps/**` 与 `packages/**`,忽略 `node_modules`、`dist`、`.next`、`.vite`、`coverage`、generated Prisma。 -- scanner 会拒绝 S2-S8 路径、路由、模型、符号泄漏,包括: - - `creation-agent/messages` - - `compile-game-ir` - - `web-runtime` - - `mini-game-conversion` - - `AIGameDesignDraft` - - `GameIRArtifact` - - `GameIR` - - `ConversionReport` - - `ValidationReport` - - `MiniGameProject` - - `MiniGameCodeConversion` - - `FeedItem` - - `GameDailyStats` - - `CreatorDailyStats` - - `/events/batch` -- `MainCreationAgentSession`、`AgentTask`、`ReviewRecord`、`LifecycleEvent` 只允许作为 S1 schema/state/audit anchor,不允许作为业务实现泄漏。 -- `packages/harness-client/**` 只在明确 `validateContract(...)` 或 harness CLI `--contract` 参数断言上下文放行 S0 contract 名称。 -- `JobExecutionStore` 是唯一 Job runtime state writer;worker 不 import Prisma/DB。 -- scanner 会拒绝 Job runtime field 的 Prisma direct write、bracket write、optional chaining write、delegate alias write,以及 raw SQL 写。 -- scanner 会拒绝 `AuditLog` update/delete/upsert、raw SQL update/delete/truncate/drop/disable trigger/function tamper。 -- scanner 会拒绝 guarded state `GameVersion` / `ReviewRecord` / `LifecycleEvent` 在 state-transition boundary 外直接写。 -- scanner 会拒绝 runtime guard setter:`set_config`、`SET LOCAL`、`app.state_transition_guard`。 -- raw SQL scanner 覆盖本轮 review 发现的绕过: - - `db.$executeRawUnsafe?.call(...)` - - `db.$executeRawUnsafe?.bind(db)(...)` - - `const execRaw = db.$executeRawUnsafe.bind(db); await execRaw(...)` - - `db. $executeRawUnsafe(...)` - - `db["$executeRawUnsafe"](...)` - - `db?.$executeRawUnsafe(...)` - - `db?.["$executeRawUnsafe"](...)` - - bracket / optional bracket 的 `call`、`bind`、bound alias - - no-semicolon / ASI bound alias -- dynamic raw SQL write identifier fail closed;dynamic read-only `SELECT` 放行。 -- `apps/api/src/modules/jobs/index.ts` 的 Task9 后续 lint 修复只是 `let cursor` -> `const cursor`,未改变 Job runtime 或 scanner 行为。 - -## Review Gate - -| Gate | Agent ID | Result | Blocking findings | +| 阶段 | Agent ID | 结果 | 关键结论 | | --- | --- | --- | --- | -| scanner implementation remediation | `019e87ab-d507-7bd0-ae38-5d537bbba64b` | DONE | 修复 Prisma optional chaining、delegate alias、raw optional/call/bind 基础覆盖 | -| scanner spec compliance | `019e87bb-5f8c-79c0-bdee-64bce4584492` | PASS | 无 | -| scanner quality / feasibility | `019e87c0-c594-7c61-8348-30ebe6332a30` | FAIL | raw SQL `?.call` / `?.bind` / bound alias 仍可绕过 | -| scanner implementation fix 1 | `019e87c7-6a70-72c0-add2-e3ae885fe774` | DONE | 修复 optional call/bind/bound alias | -| scanner spec compliance recheck 1 | `019e87d1-1965-74f3-9b22-c976be68d8ac` | PASS | 无 | -| scanner quality / feasibility recheck 1 | `019e87d5-9210-7611-b41d-162d50adb479` | FAIL | raw executor access 漏掉 dot whitespace、static bracket、optional receiver | -| scanner implementation fix 2 | `019e87e0-57e2-70b1-a272-4fa5604c8390` | DONE | 扩展 raw executor access matching | -| scanner spec compliance recheck 2 | `019e87e6-2508-7931-809b-7a5238233ca8` | FAIL | no-semicolon / ASI bound alias 漏报 | -| scanner implementation fix 3 | `019e87ea-a3f0-7273-9a02-877b378b5426` | DONE | 修复 no-semicolon / ASI bound alias | -| scanner spec compliance final | `019e8817-261a-70a3-af70-e358e55156ca` | PASS | 无 | -| scanner quality / feasibility final | `019e881e-d4ea-70b1-aa23-87febde47014` | PASS | 无 | -| lint implementation fix | `019e882a-e2ca-7093-b23e-4c1c603f14aa` | DONE | 修复 `prefer-const` | -| lint fix spec compliance | `019e8835-22cc-7782-82f6-abcc9e4e2699` | PASS | 无 | -| lint fix quality / feasibility | `019e8838-65a7-7d61-8b25-adfa4187208d` | PASS | 无 | -| S1 final read-only review | `019e883c-91a6-7542-84c1-d1c4464e831e` | PASS | 无 Critical / Important,可以进入 S2 | +| 实现链 | `019e8dfd-e640-76c2-94b1-987ef98d9125` | DONE | 复用同一实现链 Mencius,多轮修复 `scripts/check-s1-scope.mjs` | +| spec review | `019e8e0a-575e-7272-b85b-2423d121d94e` | FAIL | Prisma `job.delete/deleteMany` 漏过 JobExecutionStore gate | +| quality review | `019e8e0a-57e4-7c80-a955-815cde0fbb39` | PASS | 无 blocker | +| spec review | `019e8e16-20aa-7070-8142-de01e6422b1b` | FAIL | Job delete 测试清理白名单误放行 `job.delete` | +| quality review | `019e8e16-2161-7822-8199-828b03658746` | FAIL | assigned bound alias、Job deleteMany 复合条件白名单过宽 | +| spec review | `019e8e26-6989-7970-b2c2-1ca8616944e3` | PASS | 无 blocker | +| quality review | `019e8e26-e5d3-7631-bf52-8aa11055fa76` | FAIL | mixed conditional Prisma bound alias fail-open | +| spec review | `019e8e35-19ee-7781-9d53-cac751949a57` | PASS | 无 blocker | +| quality review | `019e8e35-1aaa-71d0-a1af-692ad62b90de` | FAIL | `Reflect.apply.bind(null)` 绕过 raw/Prisma gate | +| spec review | `019e8e3f-37df-7d21-820d-278813d74748` | FAIL | plain Prisma method alias 未追踪 | +| quality review | `019e8e3f-386f-7641-8e6a-c158a2bb5a09` | FAIL | raw/delegate/plain method conditional/logical alias fail-open | +| spec review | `019e8e60-909a-7542-8633-38543d7ae2eb` | FAIL | S2 denylist 漏 `PromptRecord` / `DesignSection` / `DesignSectionPatch` | +| quality review | `019e8e60-9125-7ee2-99a5-c2ab8c29d2bf` | PASS | 无 blocker | +| spec review | `019e8e67-abbd-72f3-bc20-2b8e714ea797` | PASS | 最终 fresh spec PASS | +| quality review | `019e8e67-add8-7710-b9f1-9080dd3c7d36` | INVALID | 返回 `completed: null`,未作为 gate 证据 | +| quality review | `019e8e6f-c282-73d1-8f86-3f7955c023d4` | TIMEOUT | 15 分钟超时,关闭后重派 | +| quality review | `019e8e7e-06b8-7f51-af03-5e243f8ffe34` | PASS | 最终 fresh quality PASS | -## Controller Verification +最终进入 full verification 的前置条件:`019e8e67-abbd-72f3-bc20-2b8e714ea797` PASS + `019e8e7e-06b8-7f51-af03-5e243f8ffe34` PASS。 -已串行运行: +## 已修 Blockers + +- block-bodied arrow function return wrapper:raw 与 Prisma guarded method/delegate return。 +- Prisma method conditional / logical bound alias。 +- Object.assign static wrapper:raw shorthand 与 Prisma method wrapper。 +- Prisma `job.delete` / `job.deleteMany` outside `JobExecutionStore`。 +- Job delete 测试清理白名单: + - `job.delete` 一律 forbidden。 + - 仅测试文件中的单一 `deleteMany({ where: { id: { startsWith: runId } } })` 放行。 + - OR / AND / NOT / extra where key / variable where / spread / computed key 均 fail closed。 +- Prisma method assigned bound alias:`let alias; alias = db.model.method.bind(...)`。 +- mixed ternary Prisma bound alias:一个分支 protected、另一个分支 safe 也 fail closed。 +- `Reflect.apply.bind(null|undefined|Reflect)` 参数位 raw/Prisma 调用。 +- plain Prisma method aliases:`const fn = db.model.method` 与 `fn = db.model.method`。 +- raw executor conditional / logical alias。 +- Prisma delegate conditional / logical alias。 +- Prisma plain method conditional / logical alias。 +- raw executor TS generic calls:direct / plain alias / bound alias / optional alias / wrapped alias。 +- S2 later-phase symbol denylist 增补: + - `PromptRecord` + - `DesignSection` + - `DesignSectionPatch` + +## Controller 验证 + +Full verification 已按顺序运行: ```bash pnpm install @@ -108,17 +93,17 @@ pnpm check:s1-scope 结果: - `pnpm install`:PASS。 - - 非阻塞提示:pnpm `10.33.0 -> 11.5.0` update notice。 + - 非阻塞提示:pnpm `10.33.0 -> 11.5.1` update notice。 - 非阻塞 warning:`msgpackr-extract@3.0.4` ignored build scripts。 - `pnpm check:workspace-scripts`:PASS。 - Docker dev services: - - `infra-postgres-1` healthy on `5432`。 - - `infra-redis-1` healthy on `6379`。 -- `pg_isready`:PASS。 -- `redis-cli ping`:`PONG`。 + - `infra-postgres-1` healthy,映射 `5432`。 + - `infra-redis-1` healthy,映射 `6379`。 +- `pg_isready`:PASS,`/var/run/postgresql:5432 - accepting connections`。 +- `redis-cli ping`:PASS,`PONG`。 - Prisma migrate deploy:PASS,3 migrations found,No pending migrations。 -- Prisma generate:PASS,Prisma Client 7.8.0 generated。 -- `pnpm lint`:初次失败于 `apps/api/src/modules/jobs/index.ts:703 prefer-const`;fresh implementer 修复并通过两轮 review 后,重跑 PASS。 +- Prisma generate:PASS,Prisma Client `7.8.0` generated。 +- `pnpm lint`:PASS。 - `pnpm typecheck`:PASS。 - `QUEUE_ADAPTER=bullmq pnpm test`:PASS。 - worker:1 file / 2 tests。 @@ -132,74 +117,65 @@ pnpm check:s1-scope - API smoke 中仍出现同一个 `pg@9` deprecation warning。 - `pnpm check:s1-scope`:PASS,`S1 scope check passed.`。 -控制器额外探针: +## Controller Targeted Probes -- optional raw call AuditLog delete:scanner failed as expected。 -- optional raw bind Job status update:scanner failed as expected。 -- bound raw alias AuditLog delete:scanner failed as expected。 -- dot whitespace raw AuditLog delete:scanner failed as expected。 -- bracket raw AuditLog delete:scanner failed as expected。 -- optional receiver / optional bracket raw write:scanner failed as expected。 -- no-semicolon dot / optional dot / bracket / optional bracket bound alias:scanner failed as expected。 -- dynamic read-only `SELECT`:scanner passed as expected。 +本轮额外用正确方式验证:在 `/tmp` 创建临时 repo root,并从该 root 作为 cwd 执行绝对路径脚本,因为 CLI 使用 `process.cwd()`。 + +已验证通过的 probe 组: + +- S2 denylist:`PromptRecord`、`DesignSection`、`DesignSectionPatch`、`AIGameDesignDraft` 均 `EXPECTED_FAIL`;S1 safe term `EXPECTED_PASS`。 +- raw TS generic:direct / nested generic / bound alias / plain alias / optional alias / wrapped alias 均 `EXPECTED_FAIL`;nonRaw generic control `EXPECTED_PASS`。 +- raw conditional/logical alias:protected 分支 `EXPECTED_FAIL`;nonRaw control `EXPECTED_PASS`。 +- Prisma delegate conditional/logical alias:Job / Audit / guarded state 均 `EXPECTED_FAIL`;safe delegate-free control `EXPECTED_PASS`。 +- Prisma plain method alias:direct / assigned / `.call` / ternary / logical 均 `EXPECTED_FAIL`;safe method control `EXPECTED_PASS`。 +- Prisma bound alias:direct / assigned / mixed ternary / logical 均 `EXPECTED_FAIL`。 +- Job delete whitelist:`delete`、non-runId cleanup、OR/AND/NOT/extra/variable/spread/computed 均 `EXPECTED_FAIL`;simple test `deleteMany({ where: { id: { startsWith: runId } } })` `EXPECTED_PASS`。 +- `Reflect.apply.bind(null|undefined|Reflect)` raw/Prisma 参数位均 `EXPECTED_FAIL`;nonPrisma control `EXPECTED_PASS`。 +- Object.assign / block-bodied arrow / function return wrappers 均按预期拦截。 +- 真实 `apps/api/prisma/migrations` 复制到 `/tmp` 后单独扫描:PASS。 ## Cleanup / Runtime State -- build/dev-smoke 产生的输出已清理: +- 已清理本轮 build/dev-smoke 产生的输出: - `apps/web/.next` - `apps/api/dist` - `apps/worker/dist` - `packages/harness-client/dist` - `packages/shared-contracts/dist` -- `apps/**` / `packages/**` 下无 `dist`、`.next` 构建输出残留。 -- `/tmp` 下无 `huijing-s1-scope-*`、`huijing-s1-probe-*`、`huijing-audit-scan-*`、`huijing-controller-s1-scope-*` 探针目录残留。 -- Docker dev services 保持运行且 healthy;未在本轮关闭。 - -## Residual Non-blocking Notes - -- 当前仓库大量 S1 workspace surface 仍是 untracked;`git diff` 对这些文件不能代表实际变更范围。 -- `.idea/`、`docs-design/` 和大量 untracked workspace surface 是前置工作面;本轮未回退或清理这些内容。 -- `pg@9` deprecation warning 仍存在,但所有相关测试命令 exit 0;升级 `pg@9` 前需要复查 Prisma adapter / 并发测试方式。 -- `msgpackr-extract@3.0.4` ignored build scripts warning 仍存在;当前验证未受影响。 -- Final reviewer 未重跑完整 `pnpm test/build`,避免重新写构建产物或扰动 dev DB;其基于控制器 full verification 与只读 spot-check 给出 PASS。 +- `apps/**` / `packages/**` 下无 `dist`、`.next`、`coverage` 残留。 +- `/tmp` 下无 `huijing-s1-*` / `huijing-audit-scan-*` / `huijing-controller-s1-scope-*` 探针目录残留。 +- Docker dev services 保持运行且 healthy;本轮未关闭。 ## SHA-256 | Path | SHA-256 | | --- | --- | -| `scripts/check-s1-scope.mjs` | `e47eee55977f28b1d711f1bc7f4e1a330c1d51291d87f6a1fd2a95d80aa738fc` | -| `apps/api/src/modules/jobs/index.ts` | `dac1176331d0adc6ba7c79f0f7788ef9efd147492d3f36d1155902784820ae32` | -| `apps/api/src/modules/assets/assets.api.spec.ts` | `815109ff2eb27cba10837af862702908c6ea06556b1ae4e75ed51a46fdaed6d0` | -| `apps/api/src/modules/audit/audit.api.spec.ts` | `efa91e6e71851bc5d8ddab0bf6b18e8c1dd4a433712529ef8d39cd877506098d` | -| `apps/api/src/modules/jobs/jobs.api.spec.ts` | `5358ac8a1bd9d66821b7c7cafd2de57a79ceeb4585b90ae9e9d06d366663fe90` | -| `apps/api/src/modules/projects/projects.api.spec.ts` | `1facfbac8e6ddb8d9bb3bed3faf69af050efafb3d7ff48c46d6a58a60d5aab32` | +| `scripts/check-s1-scope.mjs` | `e43e22ed7c38a51accdf73dfe6df68b063de1e15c37499470270c2f3fd45e263` | | `docs/superpowers/plans/2026-05-31-mvp-S1-app-foundation.md` | `868dc63fee64e254cf267d63db472aa46abf15977f1999785cfaea6fa1956a0a` | | `docs/superpowers/specs/2026-05-31-mvp-S1-app-foundation-design.md` | `f514de72c36c5fd1ee6f3e87999b911f33d392549dbb76c6ee5d538dac774cdb` | | `docs/superpowers/plans/2026-06-01-implementation-review-gate-protocol.md` | `c8726acc5457ab87fb8f26c2e439e1d1b98df0255af78e4d6e8d9636dc518478` | +| `package.json` | `0e3d6edb70fe2317497ce883710d961eaa3df9b5b37cd0e2b53873633dabe92b` | +| `pnpm-lock.yaml` | `293cd73e9a8f692e6444152a983664c3520f1f8d1d956151b453a213b79069eb` | ## Git Status Snapshot -`git status --short` 显示: +更新本文档前 `git status --short --untracked-files=all`: ```text -A .idea/.gitignore -A .idea/games-development-ai.iml -A .idea/misc.xml -A .idea/modules.xml -A .idea/vcs.xml -A docs-design/* -AM docs-design/Deployment for AI.md -?? .gitignore -?? apps/ -?? docs/ -?? eslint.config.mjs -?? harness/ -?? infra/ -?? package.json -?? packages/ -?? pnpm-lock.yaml -?? pnpm-workspace.yaml -?? scripts/ -?? tsconfig.base.json + M docs/superpowers/plans/2026-05-31-mvp-S2-creator-workbench.md + M docs/superpowers/specs/2026-05-31-mvp-S2-creator-workbench-design.md + M scripts/check-s1-scope.mjs ``` +说明: + +- 当前无 untracked 文件显示。 +- 两个 S2 文档来自既有 dirty baseline,本轮仅在整理提交阶段做文档自洽修正,仍属于当前准备提交范围。 +- 本文档更新后,`docs/memorys/2026-06-02-S1Task9最终验证门禁.md` 也会显示为本轮修改。 + +## Residual Notes + +- `pg@9` deprecation warning 仍存在,但 `QUEUE_ADAPTER=bullmq pnpm test` 与 `pnpm dev:smoke` 均 exit 0。 +- `msgpackr-extract@3.0.4` ignored build scripts warning 仍存在,当前验证未受影响。 +- `node_modules/.pnpm/bullmq@5.77.6/node_modules/bullmq/dist/esm/tsconfig.tsbuildinfo` 位于依赖目录内,本轮未清理。 +- 本轮完成时间记录:`2026-06-04 01:23:01 CST`。 diff --git a/scripts/check-s1-scope.mjs b/scripts/check-s1-scope.mjs index 70eb9c07..89a1438d 100644 --- a/scripts/check-s1-scope.mjs +++ b/scripts/check-s1-scope.mjs @@ -16,7 +16,9 @@ const historicalPrismaSchemaTest = "apps/api/src/prisma.schema.spec.ts"; const auditTriggerAssertionFiles = new Set([historicalPrismaSchemaTest, "apps/api/src/modules/audit/audit.spec.ts"]); const stateTransitionImportFixtureTests = new Set([ "apps/api/src/modules/jobs/jobs.spec.ts", - "apps/api/src/modules/jobs/jobs.api.spec.ts" + "apps/api/src/modules/jobs/jobs.api.spec.ts", + "apps/api/src/modules/projects/api-runtime.spec.ts", + "apps/api/src/modules/projects/projects.api.spec.ts" ]); const laterPhaseDeniedTerms = [ @@ -24,7 +26,10 @@ const laterPhaseDeniedTerms = [ "compile-game-ir", "web-runtime", "mini-game-conversion", + "PromptRecord", "AIGameDesignDraft", + "DesignSectionPatch", + "DesignSection", "GameIRArtifact", "GameIR", "ConversionReport", @@ -63,10 +68,14 @@ const dynamicSqlIdentifierMarker = "__HUJING_DYNAMIC_SQL_IDENTIFIER__"; const auditLogAppendOnlyFunctionName = "app_reject_audit_log_mutation"; const auditPrismaMutationMethods = ["update", "updateMany", "updateManyAndReturn", "upsert", "delete", "deleteMany"]; const guardedPrismaWriteMethods = ["create", "update", "upsert", "createMany", "updateMany", "delete", "deleteMany"]; +const guardedPrismaWriteModels = ["gameVersion", "reviewRecord", "lifecycleEvent"]; +const guardedStateSqlTables = ["GameVersion", "ReviewRecord", "LifecycleEvent"]; const jobPrismaWriteMethods = ["create", "createMany", "createManyAndReturn", "update", "updateMany", "updateManyAndReturn", "upsert"]; +const jobPrismaDeleteMethods = ["delete", "deleteMany"]; const s1AnchorTerms = ["MainCreationAgentSession", "AgentTask", "ReviewRecord", "LifecycleEvent"]; -const allowedStateTransitionImportPrefixes = [stateTransitionModulePrefix, "apps/api/src/modules/projects/"]; +const allowedStateTransitionImportPaths = new Set(["apps/api/src/modules/projects/index.ts"]); +const allowedStateTransitionImportPrefixes = [stateTransitionModulePrefix]; function normalizePath(filePath) { return filePath.split(path.sep).join("/"); @@ -223,9 +232,69 @@ function isGeneratedPrismaFile(filePath) { return filePath.startsWith(generatedPrismaPrefix); } -function allowsAuditLogAppendOnlyFunctionDefinition(filePath) { - // S1 初始 migration 安装 append-only trigger function;后续 DDL 或运行时代码仍按 denylist 扫描。 - return filePath === initialAuditLogAppendOnlyFunctionMigration; +function isPrismaMigrationSqlPath(filePath) { + return filePath.startsWith(prismaMigrationPrefix) && filePath.endsWith("/migration.sql"); +} + +function auditLogAppendOnlyFunctionCreatePattern() { + const auditLogAppendOnlyFunction = sqlFunctionReferencePattern(auditLogAppendOnlyFunctionName); + + return new RegExp(String.raw`\bCREATE\s+(OR\s+REPLACE\s+)?FUNCTION\s+${auditLogAppendOnlyFunction}\b`, "gi"); +} + +function auditLogAppendOnlyFunctionInstallPattern() { + const auditLogAppendOnlyFunction = sqlFunctionReferencePattern(auditLogAppendOnlyFunctionName); + const dollarQuote = String.raw`(?:\$\$|\$[A-Za-z_][\w$]*\$)`; + + return new RegExp( + String.raw`\bCREATE\s+(OR\s+REPLACE\s+)?FUNCTION\s+${auditLogAppendOnlyFunction}\s+RETURNS\s+trigger\s+AS\s+(${dollarQuote})([\s\S]*?)\2\s+LANGUAGE\s+plpgsql\b`, + "gi" + ); +} + +function auditLogAppendOnlyFunctionCreateStatements(sqlSource) { + const sql = stripSqlComments(sqlSource); + return [...sql.matchAll(auditLogAppendOnlyFunctionCreatePattern())]; +} + +function auditLogAppendOnlyFunctionDefinitions(sqlSource) { + const sql = stripSqlComments(sqlSource); + return [...sql.matchAll(auditLogAppendOnlyFunctionInstallPattern())].map((match) => ({ + isCreateOrReplace: Boolean(match[1]), + body: match[3] + })); +} + +function isCanonicalAuditLogAppendOnlyFunctionBody(functionBody) { + // 允许 SQL 关键字大小写与空白变化,但函数体只能安装固定 append-only guard,不能夹带 RETURN/IF/EXECUTE 等语句。 + return /^\s*BEGIN\s+RAISE\s+EXCEPTION\s+'AuditLog is append-only'\s+USING\s+ERRCODE\s*=\s*'42501'\s*;\s*END\s*;?\s*$/i.test( + functionBody + ); +} + +function hasAllowedAuditLogAppendOnlyFunctionInstall(sqlSource, options = {}) { + const createStatements = auditLogAppendOnlyFunctionCreateStatements(sqlSource); + if (createStatements.length !== 1) return false; + + const definitions = auditLogAppendOnlyFunctionDefinitions(sqlSource); + if (definitions.length !== 1) return false; + + const [definition] = definitions; + if (definition.isCreateOrReplace && !options.allowOrReplace) return false; + + return isCanonicalAuditLogAppendOnlyFunctionBody(definition.body); +} + +function auditLogAppendOnlyFunctionCreateOptions(filePath, content) { + return { + // 后续 migration 只允许 plain CREATE FUNCTION 安装固定 append-only guard,不允许替换已有函数。 + allowAuditLogAppendOnlyFunctionPlainCreate: + isPrismaMigrationSqlPath(filePath) && hasAllowedAuditLogAppendOnlyFunctionInstall(content), + // 真实 S1 初始 migration 历史上使用 CREATE OR REPLACE;仅该文件按同一函数体语义兼容放行。 + allowAuditLogAppendOnlyFunctionCreateOrReplace: + filePath === initialAuditLogAppendOnlyFunctionMigration && + hasAllowedAuditLogAppendOnlyFunctionInstall(content, { allowOrReplace: true }) + }; } function isAllowedAuditTriggerAssertion(filePath, content, structure, mutationIndex) { @@ -258,7 +327,7 @@ function isAllowedAuditTriggerAssertion(filePath, content, structure, mutationIn function hasForbiddenAuditMutation(filePath, content) { const structure = stripStringLiteralsExceptPropertyKeys(maskCommentsOutsideStringLiterals(content)); - for (const match of prismaModelMethodCallMatchesInStructure(structure, ["auditLog"], auditPrismaMutationMethods)) { + for (const match of prismaModelMethodCallMatchesInStructure(content, structure, ["auditLog"], auditPrismaMutationMethods)) { if (typeof match.index === "number" && isAllowedAuditTriggerAssertion(filePath, content, structure, match.index)) continue; return true; } @@ -266,36 +335,421 @@ function hasForbiddenAuditMutation(filePath, content) { return hasForbiddenRawAuditLogMutation(filePath, content, structure); } -function hasGuardedPrismaWrite(content) { - const guardedModels = ["gameVersion", "reviewRecord", "lifecycleEvent"]; - const hasPrismaWrite = prismaModelMethodCallMatches(content, guardedModels, guardedPrismaWriteMethods).length > 0; - const hasSqlWrite = /\b(?:INSERT\s+INTO|UPDATE|DELETE\s+FROM)\s+"?(?:GameVersion|ReviewRecord|LifecycleEvent)"?/i.test( - content +function guardedStateSqlTableReferencePattern() { + return String.raw`(?:${guardedStateSqlTables.map(sqlTableReferencePattern).join("|")})`; +} + +function hasRawGuardedStateWrite(sqlSource) { + const sql = normalizeRawSqlScanSource(sqlSource); + const guardedTable = guardedStateSqlTableReferencePattern(); + const identifier = sqlBareIdentifierPattern(); + + // guarded state 的 raw SQL 必须跟 Prisma raw invocation 同源扫描,覆盖转义字符串、SQL 注释和 Postgres U& identifier。 + return ( + new RegExp(String.raw`\bUPDATE\s+(?:ONLY\s+)?${guardedTable}(?:\s*\*)?(?:\s+(?:AS\s+)?${identifier})?\s+SET\b`, "i").test( + sql + ) || + new RegExp(String.raw`\bINSERT\s+INTO\s+(?:ONLY\s+)?${guardedTable}\b`, "i").test(sql) || + new RegExp(String.raw`\bDELETE\s+FROM\s+(?:ONLY\s+)?${guardedTable}(?:\s*\*)?(?:\s+(?:AS\s+)?${identifier})?\b`, "i").test( + sql + ) || + new RegExp(String.raw`\bMERGE\s+INTO\s+(?:ONLY\s+)?${guardedTable}(?:\s*\*)?(?:\s+(?:AS\s+)?${identifier})?\b`, "i").test( + sql + ) || + new RegExp(sqlTruncateTableListPattern(guardedTable), "i").test(sql) ); - return hasPrismaWrite || hasSqlWrite; +} + +function hasGuardedPrismaWrite(filePath, content) { + const hasPrismaWrite = prismaModelMethodCallMatches(content, guardedPrismaWriteModels, guardedPrismaWriteMethods).length > 0; + const hasRawSqlWrite = filePath.endsWith(".sql") + ? hasRawGuardedStateWrite(content) + : hasRawSqlInvocationMatching(content, hasRawGuardedStateWrite); + return hasPrismaWrite || hasRawSqlWrite; } function hasRuntimeGuardSetter(content) { return /app\.state_transition_guard|set_config\s*\(|SET\s+LOCAL/i.test(content); } +function isHexDigits(value) { + return /^[0-9a-f]+$/i.test(value); +} + +function decodeJavaScriptStringLiteralBody(body) { + let output = ""; + + for (let index = 0; index < body.length; index += 1) { + const char = body[index]; + if (char !== "\\") { + output += char; + continue; + } + + const next = body[index + 1]; + if (typeof next === "undefined") { + output += char; + continue; + } + + if (next === "\r" && body[index + 2] === "\n") { + index += 2; + continue; + } + + if (next === "\n" || next === "\r") { + index += 1; + continue; + } + + if (next === "x") { + const hexValue = body.slice(index + 2, index + 4); + if (hexValue.length === 2 && isHexDigits(hexValue)) { + output += String.fromCharCode(Number.parseInt(hexValue, 16)); + index += 3; + continue; + } + } + + if (next === "u") { + if (body[index + 2] === "{") { + const closeBraceIndex = body.indexOf("}", index + 3); + const hexValue = closeBraceIndex === -1 ? "" : body.slice(index + 3, closeBraceIndex); + const codePoint = hexValue.length > 0 && isHexDigits(hexValue) ? Number.parseInt(hexValue, 16) : Number.NaN; + + if (codePoint >= 0 && codePoint <= 0x10ffff) { + output += String.fromCodePoint(codePoint); + index = closeBraceIndex; + continue; + } + } + + const hexValue = body.slice(index + 2, index + 6); + if (hexValue.length === 4 && isHexDigits(hexValue)) { + output += String.fromCharCode(Number.parseInt(hexValue, 16)); + index += 5; + continue; + } + } + + const escapedCharacters = { + "\\": "\\", + '"': '"', + "'": "'", + "`": "`", + n: "\n", + r: "\r", + t: "\t", + b: "\b", + f: "\f", + v: "\v", + 0: "\0" + }; + + output += Object.hasOwn(escapedCharacters, next) ? escapedCharacters[next] : next; + index += 1; + } + + return output; +} + +function decodeStaticJavaScriptStringLiteralSource(source) { + const literalStart = skipWhitespaceForward(source, 0); + const literalEndBoundary = skipWhitespaceBackward(source, source.length - 1); + if (literalStart > literalEndBoundary) return null; + + const quote = source[literalStart]; + if (quote !== '"' && quote !== "'" && quote !== "`") return null; + + const literalEnd = findStringLiteralEnd(source, literalStart); + if (literalEnd !== literalEndBoundary) return null; + + const body = source.slice(literalStart + 1, literalEnd); + // 带表达式的 template literal 继续走原有动态 SQL 归一化路径,避免把参数表达式误解成 SQL 文本。 + if (quote === "`" && /\$\{/.test(body)) return null; + + return decodeJavaScriptStringLiteralBody(body); +} + +function decodeStaticJavaScriptStringLiteralsInSource(source) { + let output = ""; + let index = 0; + + while (index < source.length) { + const quote = source[index]; + if ( + quote === '"' && + source[index - 1] === "&" && + (source[index - 2] === "U" || source[index - 2] === "u") + ) { + let cursor = index + 1; + while (cursor < source.length) { + if (source[cursor] === '"' && source[cursor + 1] === '"') { + cursor += 2; + continue; + } + cursor += 1; + if (source[cursor - 1] === '"') break; + } + + output += source.slice(index, cursor); + index = cursor; + continue; + } + + if (quote !== '"' && quote !== "'" && quote !== "`") { + output += quote; + index += 1; + continue; + } + + const literalEnd = findStringLiteralEnd(source, index); + if (literalEnd === -1) { + output += source.slice(index); + break; + } + + const body = source.slice(index + 1, literalEnd); + // 动态 template literal 的表达式仍交给 dynamic SQL marker,不能在这里静态展开。 + output += quote === "`" && /\$\{/.test(body) ? source.slice(index, literalEnd + 1) : `${quote}${decodeJavaScriptStringLiteralBody(body)}${quote}`; + index = literalEnd + 1; + } + + return output; +} + +function sqlDollarQuoteTagAt(content, index) { + return /^(?:\$\$|\$[A-Za-z_][\w$]*\$)/.exec(content.slice(index))?.[0] ?? null; +} + +function decodePostgresUnicodeQuotedIdentifierBody(body, escapeChar = "\\") { + let output = ""; + + for (let index = 0; index < body.length; index += 1) { + const char = body[index]; + if (char !== escapeChar) { + output += char; + continue; + } + + if (body[index + 1] === "+") { + const hexValue = body.slice(index + 2, index + 8); + const codePoint = hexValue.length === 6 && isHexDigits(hexValue) ? Number.parseInt(hexValue, 16) : Number.NaN; + if (codePoint >= 0 && codePoint <= 0x10ffff) { + output += String.fromCodePoint(codePoint); + index += 7; + continue; + } + } + + const hexValue = body.slice(index + 1, index + 5); + if (hexValue.length === 4 && isHexDigits(hexValue)) { + output += String.fromCharCode(Number.parseInt(hexValue, 16)); + index += 4; + continue; + } + + output += char; + } + + return output; +} + +function postgresUnicodeQuotedIdentifierEscapeClauseAt(content, index) { + const keywordStart = skipWhitespaceForward(content, index); + const keywordMatch = /^UESCAPE\b/i.exec(content.slice(keywordStart)); + if (!keywordMatch) return null; + + const quoteStart = skipWhitespaceForward(content, keywordStart + keywordMatch[0].length); + if (content[quoteStart] !== "'") return null; + + const escapeChar = content[quoteStart + 1]; + if (typeof escapeChar === "undefined" || content[quoteStart + 2] !== "'") return null; + + return { escapeChar, end: quoteStart + 3 }; +} + +function normalizePostgresUnicodeQuotedIdentifiers(content) { + let output = ""; + let index = 0; + + while (index < content.length) { + if ((content[index] !== "U" && content[index] !== "u") || content[index + 1] !== "&" || content[index + 2] !== '"') { + output += content[index]; + index += 1; + continue; + } + + let body = ""; + let cursor = index + 3; + let closeQuoteIndex = -1; + while (cursor < content.length) { + const char = content[cursor]; + const next = content[cursor + 1]; + if (char === '"' && next === '"') { + body += '""'; + cursor += 2; + continue; + } + if (char === '"') { + closeQuoteIndex = cursor; + break; + } + body += char; + cursor += 1; + } + + if (closeQuoteIndex === -1) { + output += content[index]; + index += 1; + continue; + } + + const escapeClause = postgresUnicodeQuotedIdentifierEscapeClauseAt(content, closeQuoteIndex + 1); + // UESCAPE 紧跟 U& 标识符时会改变转义字符;归一化后必须移除该子句,避免污染表名模式。 + output += `"${decodePostgresUnicodeQuotedIdentifierBody(body, escapeClause?.escapeChar ?? "\\")}"`; + index = escapeClause?.end ?? closeQuoteIndex + 1; + } + + return output; +} + function stripSqlComments(content) { - return content - .replace(/\/\*[\s\S]*?\*\//g, "") - .split("\n") - .map((line) => line.replace(/--.*$/, "")) - .join("\n"); + let output = ""; + let index = 0; + + while (index < content.length) { + const char = content[index]; + const next = content[index + 1]; + + if (char === "-" && next === "-") { + // 真实行注释替换为空白但保留换行,避免擦掉下一条 SQL 语句的行边界。 + while (index < content.length && content[index] !== "\n" && content[index] !== "\r") { + output += " "; + index += 1; + } + continue; + } + + if (char === "/" && next === "*") { + // 注释可能夹在关键字之间,必须保留 token separator,避免 DELETE/*x*/FROM 被拼成 DELETEFROM。 + let depth = 1; + output += " "; + index += 2; + + while (index < content.length && depth > 0) { + const blockChar = content[index]; + const blockNext = content[index + 1]; + if (blockChar === "/" && blockNext === "*") { + output += " "; + index += 2; + depth += 1; + continue; + } + + if (blockChar === "*" && blockNext === "/") { + output += " "; + index += 2; + depth -= 1; + continue; + } + + output += blockChar === "\n" || blockChar === "\r" ? blockChar : " "; + index += 1; + } + continue; + } + + if (char === "'") { + output += char; + index += 1; + + while (index < content.length) { + const stringChar = content[index]; + const stringNext = content[index + 1]; + output += stringChar; + + if (stringChar === "\\") { + if (typeof stringNext !== "undefined") output += stringNext; + index += typeof stringNext === "undefined" ? 1 : 2; + continue; + } + + if (stringChar === "'" && stringNext === "'") { + output += stringNext; + index += 2; + continue; + } + + index += 1; + if (stringChar === "'") break; + } + + continue; + } + + if (char === '"') { + output += char; + index += 1; + + while (index < content.length) { + const identifierChar = content[index]; + const identifierNext = content[index + 1]; + output += identifierChar; + + if (identifierChar === "\\") { + if (typeof identifierNext !== "undefined") output += identifierNext; + index += typeof identifierNext === "undefined" ? 1 : 2; + continue; + } + + if (identifierChar === '"' && identifierNext === '"') { + output += identifierNext; + index += 2; + continue; + } + + index += 1; + if (identifierChar === '"') break; + } + + continue; + } + + const dollarQuoteTag = char === "$" ? sqlDollarQuoteTagAt(content, index) : null; + if (dollarQuoteTag) { + const dollarQuoteEnd = content.indexOf(dollarQuoteTag, index + dollarQuoteTag.length); + if (dollarQuoteEnd === -1) { + output += content.slice(index); + break; + } + + output += content.slice(index, dollarQuoteEnd + dollarQuoteTag.length); + index = dollarQuoteEnd + dollarQuoteTag.length; + continue; + } + + output += char; + index += 1; + } + + return output; } function normalizeRawSqlScanSource(content) { - return stripSqlComments(content) + const sqlSource = decodeStaticJavaScriptStringLiteralSource(content) ?? decodeStaticJavaScriptStringLiteralsInSource(content); + + return normalizePostgresUnicodeQuotedIdentifiers(stripSqlComments(sqlSource)) .replace(/\\(["'`])/g, "$1") .replace(/\\n/g, "\n") .replace(/\\r/g, "\r") .replace(/\\t/g, "\t") .replace( /\$\{\s*Prisma\s*\.\s*raw\s*\(\s*((?:"(?:\\.|[^"\\])*")|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`)\s*\)\s*\}/gs, - " $1 " + (_match, literalSource) => { + // Prisma.raw(...) 的静态 identifier 片段也必须按 JS 运行时字符串解码后再进入 SQL 归一化。 + return ` ${decodeStaticJavaScriptStringLiteralSource(literalSource) ?? literalSource} `; + } ) .replace(/\$\{[\s\S]*?\}/g, " ? ") .replace(/["'`]/g, " ") @@ -318,6 +772,21 @@ function sqlFunctionReferencePattern(functionName) { return String.raw`(?:${identifier}\s*\.\s*)?${escapeRegExp(functionName)}\s*(?:\([^)]*\))?`; } +function sqlSpecificFunctionReferencePattern(functionName) { + const identifier = sqlBareIdentifierPattern(); + return String.raw`(?:${identifier}\s*\.\s*)?${escapeRegExp(functionName)}\b\s*(?:\([^)]*\))?`; +} + +function sqlAnyFunctionReferencePattern() { + const identifier = sqlBareIdentifierPattern(); + return String.raw`(?:${identifier}\s*\.\s*)?${identifier}\s*(?:\([^)]*\))?`; +} + +function sqlFunctionListContainingPattern(targetFunctionReference) { + const functionReference = sqlAnyFunctionReferencePattern(); + return String.raw`(?:${functionReference}\s*,\s*)*${targetFunctionReference}(?:\s*,\s*${functionReference})*`; +} + function staticStringLiteralAlternatives(values) { return values.flatMap((value) => { const escapedValue = escapeRegExp(value); @@ -331,6 +800,1554 @@ function staticPrismaPropertyAccessPattern(values) { return String.raw`(?:(?:\.|\?\.)\s*(?:${dotNames})|(?:\?\.\s*)?\[\s*(?:${bracketNames})\s*\])`; } +function staticPrismaPropertyAccessMatchAt(content, structure, index, values, beforeIndex = index) { + const allowedValues = new Set(values); + let propertyStart = index; + + if (structure.startsWith("?.", index)) { + propertyStart = skipWhitespaceForward(structure, index + 2); + } else if (structure[index] === ".") { + propertyStart = skipWhitespaceForward(structure, index + 1); + } + + if (structure[propertyStart] === "[") { + const closeBracketIndex = findMatchingBracket(structure, propertyStart); + if (closeBracketIndex === -1) return null; + + const expressionStart = propertyStart + 1; + const expressionEnd = closeBracketIndex; + const resolvedValues = staticJavaScriptExpressionValues( + content, + structure, + content.slice(expressionStart, expressionEnd), + expressionStart, + beforeIndex, + new Set(), + 0 + ); + const value = resolvedValues.find((candidate) => allowedValues.has(candidate)); + return value ? { index, end: closeBracketIndex + 1, value } : null; + } + + if (propertyStart === index && structure[index] !== "." && !structure.startsWith("?.", index)) return null; + + const nameMatch = /^[A-Za-z_$][\w$]*/.exec(structure.slice(propertyStart)); + if (!nameMatch || !allowedValues.has(nameMatch[0])) return null; + + return { index, end: propertyStart + nameMatch[0].length, value: nameMatch[0] }; +} + +function staticPrismaPropertyAccessMatches(content, structure, values, startIndex = 0, endIndex = structure.length) { + const matches = []; + + for (let cursor = startIndex; cursor < endIndex; cursor += 1) { + if (structure[cursor] !== "." && structure[cursor] !== "[" && !structure.startsWith("?.", cursor)) continue; + + const match = staticPrismaPropertyAccessMatchAt(content, structure, cursor, values, cursor); + if (!match || match.end > endIndex) continue; + + // computed key 统一走静态 JS 表达式解析,覆盖多段拼接、变量 alias 和完整括号表达式。 + matches.push(match); + cursor = match.end - 1; + } + + return matches; +} + +function staticPrismaDelegateExpressionMatchAt(content, structure, index, models) { + const receiverMatch = /^(?:[A-Za-z_$][\w$]*|this)\b/.exec(structure.slice(index)); + if (!receiverMatch) return null; + + const modelAccessStart = skipWhitespaceForward(structure, index + receiverMatch[0].length); + const modelAccess = staticPrismaPropertyAccessMatchAt(content, structure, modelAccessStart, models, modelAccessStart); + return modelAccess ? { index, end: modelAccess.end, model: modelAccess.value } : null; +} + +function hasOptionalCallOperatorAt(structure, index) { + const callStart = skipWhitespaceForward(structure, index); + if (structure.startsWith("?.", callStart)) { + return skipWhitespaceForward(structure, callStart + 2); + } + return callStart; +} + +function functionPrototypeMethodKind(structure, sourceStart, sourceEnd) { + const start = skipWhitespaceForward(structure, sourceStart); + const end = skipWhitespaceBackward(structure, sourceEnd - 1) + 1; + if (start >= end) return null; + + const normalizedSource = structure.slice(start, end).replace(/\s+/g, ""); + if (normalizedSource === "Function.prototype.call") return "call"; + if (normalizedSource === "Function.prototype.apply") return "apply"; + return null; +} + +function staticBuiltinInvokerKind(structure, sourceStart, sourceEnd) { + const functionPrototypeKind = functionPrototypeMethodKind(structure, sourceStart, sourceEnd); + if (functionPrototypeKind) return functionPrototypeKind; + + const start = skipWhitespaceForward(structure, sourceStart); + const end = skipWhitespaceBackward(structure, sourceEnd - 1) + 1; + if (start >= end) return null; + + const normalizedSource = structure.slice(start, end).replace(/\s+/g, ""); + return normalizedSource === "Reflect.apply" ? "reflectApply" : null; +} + +function staticBuiltinInvokerBaseExpressionMatchAt(structure, index) { + return staticBuiltinInvokerBaseExpressionMatchAtWithContent("", structure, index); +} + +function staticBuiltinInvokerBaseExpressionMatchAtWithContent(content, structure, index) { + const expressionStart = skipWhitespaceForward(structure, index); + const rootMatch = /^(?:Function|Reflect)\b/.exec(structure.slice(expressionStart)); + if (!rootMatch) return null; + + const firstAccessStart = skipWhitespaceForward(structure, expressionStart + rootMatch[0].length); + const firstAccess = staticStaticMemberPropertyAccessMatchAt(content, structure, firstAccessStart); + if (!firstAccess) return null; + + if (rootMatch[0] === "Reflect") { + return firstAccess.value === "apply" ? { kind: "reflectApply", end: firstAccess.end } : null; + } + + if (firstAccess.value !== "prototype") return null; + + const secondAccessStart = skipWhitespaceForward(structure, firstAccess.end); + const secondAccess = staticStaticMemberPropertyAccessMatchAt(content, structure, secondAccessStart); + if (!secondAccess || (secondAccess.value !== "call" && secondAccess.value !== "apply")) return null; + + return { kind: secondAccess.value, end: secondAccess.end }; +} + +function staticMemberOperationMatchAt(content, structure, index, values) { + const accessStart = skipWhitespaceForward(structure, index); + const access = staticStaticMemberPropertyAccessMatchAt(content, structure, accessStart); + if (!access || !values.includes(access.value)) return null; + + return { value: access.value, end: access.end }; +} + +function staticBuiltinMemberOperationMatchAt(content, structure, index, values) { + return staticMemberOperationMatchAt(content, structure, index, values); +} + +function staticCallableMemberOperationMatchAt(content, structure, index, values) { + const operation = staticMemberOperationMatchAt(content, structure, index, values); + if (!operation) return null; + + const openParenIndex = skipOptionalTypeArguments(structure, operation.end); + if (structure[openParenIndex] !== "(") return null; + + const closeParenIndex = findMatchingParen(structure, openParenIndex); + if (closeParenIndex === -1) return null; + + return { kind: operation.value, methodNameEnd: operation.end, openParenIndex, closeParenIndex }; +} + +function isStaticBuiltinInvokerBindReceiver(content, structure, kind, receiverArgument) { + if (!receiverArgument) return false; + + const receiverStart = skipWhitespaceForward(structure, receiverArgument.index); + const receiverEnd = skipWhitespaceBackward(structure, receiverArgument.end - 1) + 1; + if (receiverStart >= receiverEnd) return false; + + if (kind === "reflectApply") { + // Reflect.apply 的 this 不参与语义,`Reflect.apply.bind(null|undefined|Reflect)` 都等价;只要 target 是静态 Reflect.apply 就登记 alias。 + return true; + } + + const receiverMatch = staticBuiltinInvokerBaseExpressionMatchAtWithContent(content, structure, receiverStart); + return receiverMatch?.kind === kind && skipWhitespaceForward(structure, receiverMatch.end) === receiverEnd; +} + +function staticBuiltinInvokerExpressionMatchAt(content, structure, index) { + const expressionStart = skipWhitespaceForward(structure, index); + const baseMatch = staticBuiltinInvokerBaseExpressionMatchAtWithContent(content, structure, expressionStart); + if (!baseMatch) return null; + + const bindMethodStart = skipWhitespaceForward(structure, baseMatch.end); + const bindMethodMatch = staticBuiltinMemberOperationMatchAt(content, structure, bindMethodStart, ["bind"]); + if (!bindMethodMatch) return { kind: baseMatch.kind, end: baseMatch.end, bound: false }; + + const bindCallStart = skipWhitespaceForward(structure, bindMethodMatch.end); + if (structure[bindCallStart] !== "(") return null; + + const bindCallEnd = findMatchingParen(structure, bindCallStart); + if (bindCallEnd === -1) return null; + + const receiverArgument = nthCallArgumentSource(content, structure, bindCallStart, bindCallEnd, 0); + if (!isStaticBuiltinInvokerBindReceiver(content, structure, baseMatch.kind, receiverArgument)) return null; + + return { kind: baseMatch.kind, end: bindCallEnd + 1, bound: true }; +} + +function findStaticBuiltinInvokerAliases(content, structure) { + const aliases = []; + const declarationPattern = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\b(?:\s*:[^=;]+)?\s*=/g; + const assignmentPattern = /\b([A-Za-z_$][\w$]*)\b\s*=/g; + + for (const declarationMatch of structure.matchAll(declarationPattern)) { + if (typeof declarationMatch.index !== "number") continue; + + const equalsIndex = structure.indexOf("=", declarationMatch.index); + if (equalsIndex === -1) continue; + + const initializerStart = skipWhitespaceForward(structure, equalsIndex + 1); + const statementEnd = findStatementEnd(structure, initializerStart); + const invokerMatch = staticBuiltinInvokerExpressionMatchAt(content, structure, initializerStart); + if (!invokerMatch) continue; + + const aliasEnd = simpleStaticAliasInitializerEnd(structure, invokerMatch.end, statementEnd); + if (aliasEnd === -1) continue; + + // 只接受同文件静态内建调用器 alias,复杂表达式不进入 scope gate 语义展开。 + aliases.push({ + name: declarationMatch[1], + kind: invokerMatch.kind, + bound: invokerMatch.bound, + index: declarationMatch.index, + end: aliasEnd + }); + } + + for (const assignmentMatch of structure.matchAll(assignmentPattern)) { + if (typeof assignmentMatch.index !== "number") continue; + + const variableName = assignmentMatch[1]; + const variableIndex = assignmentMatch.index + assignmentMatch[0].indexOf(variableName); + if (isMemberIdentifierReference(structure, variableIndex)) continue; + if (/\b(?:const|let|var)\s*$/.test(structure.slice(Math.max(0, assignmentMatch.index - 16), variableIndex))) continue; + + const equalsIndex = structure.indexOf("=", assignmentMatch.index + assignmentMatch[0].lastIndexOf("=")); + if (equalsIndex === -1) continue; + if ("=>"].includes(structure[equalsIndex + 1] ?? "")) continue; + + const initializerStart = skipWhitespaceForward(structure, equalsIndex + 1); + const statementEnd = findStatementEnd(structure, initializerStart); + const invokerMatch = staticBuiltinInvokerExpressionMatchAt(content, structure, initializerStart); + if (!invokerMatch) continue; + + const aliasEnd = simpleStaticAliasInitializerEnd(structure, invokerMatch.end, statementEnd); + if (aliasEnd === -1) continue; + + // 赋值 alias 覆盖 `let c; c = Function.prototype.call` 这类后续绑定。 + aliases.push({ name: variableName, kind: invokerMatch.kind, bound: invokerMatch.bound, index: assignmentMatch.index, end: aliasEnd }); + } + + return propagateStaticIdentifierAliases(structure, aliases, (sourceAlias, initializer) => ({ + name: initializer.name, + kind: sourceAlias.kind, + bound: sourceAlias.bound, + index: initializer.index, + end: initializer.end + })); +} + +function hasSimpleIdentifierAssignmentBetween(structure, variableName, startIndex, beforeIndex) { + const assignmentPattern = new RegExp(String.raw`\b${escapeRegExp(variableName)}\b\s*=`, "g"); + + for (const assignmentMatch of structure.matchAll(assignmentPattern)) { + if (typeof assignmentMatch.index !== "number" || assignmentMatch.index <= startIndex || assignmentMatch.index >= beforeIndex) continue; + + const variableIndex = assignmentMatch.index + assignmentMatch[0].indexOf(variableName); + if (isMemberIdentifierReference(structure, variableIndex)) continue; + + const equalsIndex = structure.indexOf("=", assignmentMatch.index + assignmentMatch[0].lastIndexOf("=")); + if (equalsIndex === -1 || equalsIndex >= beforeIndex) continue; + if ("=>"].includes(structure[equalsIndex + 1] ?? "")) continue; + + return true; + } + + return false; +} + +function latestStaticAliasRecordAt(structure, aliases, aliasName, beforeIndex) { + const latestAlias = aliases + .filter((alias) => alias.name === aliasName && (alias.end ?? alias.index) < beforeIndex) + .toSorted((left, right) => (right.end ?? right.index) - (left.end ?? left.index))[0]; + + if (!latestAlias) return null; + if (hasSimpleIdentifierAssignmentBetween(structure, aliasName, latestAlias.end ?? latestAlias.index, beforeIndex)) return null; + + return latestAlias; +} + +function simpleIdentifierAliasInitializers(structure) { + const aliases = []; + const declarationPattern = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\b(?:\s*:[^=;]+)?\s*=/g; + const assignmentPattern = /\b([A-Za-z_$][\w$]*)\b\s*=/g; + + for (const declarationMatch of structure.matchAll(declarationPattern)) { + if (typeof declarationMatch.index !== "number") continue; + + const equalsIndex = structure.indexOf("=", declarationMatch.index); + if (equalsIndex === -1) continue; + + const initializerStart = skipWhitespaceForward(structure, equalsIndex + 1); + const initializerMatch = /^[A-Za-z_$][\w$]*/.exec(structure.slice(initializerStart)); + if (!initializerMatch) continue; + + const initializerEnd = initializerStart + initializerMatch[0].length; + const statementEnd = findStatementEnd(structure, initializerStart); + const aliasEnd = simpleStaticAliasInitializerEnd(structure, initializerEnd, statementEnd); + if (aliasEnd === -1) continue; + + aliases.push({ + name: declarationMatch[1], + initializerName: initializerMatch[0], + index: declarationMatch.index, + initializerStart, + end: aliasEnd + }); + } + + for (const assignmentMatch of structure.matchAll(assignmentPattern)) { + if (typeof assignmentMatch.index !== "number") continue; + + const variableName = assignmentMatch[1]; + const variableIndex = assignmentMatch.index + assignmentMatch[0].indexOf(variableName); + if (isMemberIdentifierReference(structure, variableIndex)) continue; + if (/\b(?:const|let|var)\s*$/.test(structure.slice(Math.max(0, assignmentMatch.index - 16), variableIndex))) continue; + + const equalsIndex = structure.indexOf("=", assignmentMatch.index + assignmentMatch[0].lastIndexOf("=")); + if (equalsIndex === -1) continue; + if ("=>"].includes(structure[equalsIndex + 1] ?? "")) continue; + + const initializerStart = skipWhitespaceForward(structure, equalsIndex + 1); + const initializerMatch = /^[A-Za-z_$][\w$]*/.exec(structure.slice(initializerStart)); + if (!initializerMatch) continue; + + const initializerEnd = initializerStart + initializerMatch[0].length; + const statementEnd = findStatementEnd(structure, initializerStart); + const aliasEnd = simpleStaticAliasInitializerEnd(structure, initializerEnd, statementEnd); + if (aliasEnd === -1) continue; + + aliases.push({ + name: variableName, + initializerName: initializerMatch[0], + index: assignmentMatch.index, + initializerStart, + end: aliasEnd + }); + } + + return aliases; +} + +function propagateStaticIdentifierAliases(structure, aliases, copyAlias) { + const propagated = [...aliases]; + const initializers = simpleIdentifierAliasInitializers(structure); + + for (let depth = 0; depth < rawSqlVariableResolutionMaxDepth; depth += 1) { + let changed = false; + + for (const initializer of initializers) { + if (initializer.name === initializer.initializerName) continue; + if (propagated.some((alias) => alias.name === initializer.name && (alias.end ?? alias.index) === initializer.end)) continue; + + const sourceAlias = latestStaticAliasRecordAt(structure, propagated, initializer.initializerName, initializer.initializerStart); + if (!sourceAlias) continue; + + propagated.push(copyAlias(sourceAlias, initializer)); + changed = true; + } + + if (!changed) break; + } + + return propagated.toSorted((left, right) => (left.end ?? left.index) - (right.end ?? right.index)); +} + +function completeStaticWrapperSourceMatch(content, structure, sourceMatcher, sourceStart, sourceEnd, beforeIndex) { + const expressionStart = skipWhitespaceForward(structure, sourceStart); + const expressionEnd = skipWhitespaceBackward(structure, sourceEnd - 1) + 1; + if (expressionStart >= expressionEnd) return null; + + const match = sourceMatcher(content, structure, expressionStart, expressionEnd, beforeIndex); + if (!match) return null; + + return isAtStaticWrapperExpressionEnd(structure, match.end, expressionEnd) ? match : null; +} + +function staticIdentifierAliasSourceMatchAt(structure, aliases, sourceStart, sourceEnd, beforeIndex) { + const aliasName = simpleIdentifierSourceName(structure, sourceStart, sourceEnd); + if (!aliasName) return null; + + const alias = latestStaticAliasRecordAt(structure, aliases, aliasName, beforeIndex); + return alias ? { index: sourceStart, end: sourceEnd, alias } : null; +} + +function isAtStaticWrapperExpressionEnd(structure, cursor, expressionEnd) { + // wrapper source 已按表达式边界 trim;结束判断不能继续越过 `}`、`]` 或 `;`。 + return cursor === expressionEnd || (cursor < expressionEnd && skipWhitespaceForward(structure, cursor) === expressionEnd); +} + +function staticWrapperSourcesInExpression(content, structure, sourceMatcher, expressionStart, expressionEnd, beforeIndex, basePath, depth) { + if (depth >= rawSqlVariableResolutionMaxDepth) return []; + + const wrappers = []; + const unwrapped = unwrapCompleteParenthesizedJavaScriptExpression(structure, expressionStart, expressionEnd); + const valueStart = skipWhitespaceForward(structure, unwrapped.start); + const valueEnd = skipWhitespaceBackward(structure, unwrapped.end - 1) + 1; + if (valueStart >= valueEnd) return wrappers; + + const sourceMatch = completeStaticWrapperSourceMatch(content, structure, sourceMatcher, valueStart, valueEnd, beforeIndex); + if (sourceMatch) { + wrappers.push({ path: basePath, source: sourceMatch }); + return wrappers; + } + + const objectAssignSources = staticObjectAssignArgumentSources(content, structure, valueStart, valueEnd); + if (objectAssignSources.length > 0) { + for (const objectAssignSource of objectAssignSources) { + wrappers.push( + ...staticWrapperSourcesInExpression( + content, + structure, + sourceMatcher, + objectAssignSource.index, + objectAssignSource.end, + beforeIndex, + basePath, + depth + 1 + ) + ); + } + return wrappers; + } + + if (structure[valueStart] === "{") { + const objectEnd = findMatchingBrace(structure, valueStart); + if (objectEnd === -1 || objectEnd !== valueEnd - 1) return wrappers; + + for (const part of destructuringParts(structure, valueStart + 1, objectEnd)) { + const propertyName = staticDestructuringPropertyKeyValue(content, structure, part.start, part.end, beforeIndex); + if (!propertyName) continue; + + const keyStart = skipWhitespaceForward(structure, part.start); + const keyEnd = destructuringPropertyKeyEnd(structure, keyStart, part.end); + if (keyEnd === -1) continue; + + const colonIndex = skipWhitespaceForward(structure, keyEnd); + if (structure[colonIndex] !== ":") { + const shorthandMatch = completeStaticWrapperSourceMatch(content, structure, sourceMatcher, keyStart, keyEnd, beforeIndex); + if (shorthandMatch) wrappers.push({ path: basePath.concat(propertyName), source: shorthandMatch }); + continue; + } + + wrappers.push( + ...staticWrapperSourcesInExpression( + content, + structure, + sourceMatcher, + skipWhitespaceForward(structure, colonIndex + 1), + part.end, + beforeIndex, + basePath.concat(propertyName), + depth + 1 + ) + ); + } + return wrappers; + } + + if (structure[valueStart] === "[") { + const arrayEnd = findMatchingBracket(structure, valueStart); + if (arrayEnd === -1 || arrayEnd !== valueEnd - 1) return wrappers; + + const parts = destructuringParts(structure, valueStart + 1, arrayEnd); + for (const [elementIndex, part] of parts.entries()) { + wrappers.push( + ...staticWrapperSourcesInExpression( + content, + structure, + sourceMatcher, + part.start, + part.end, + beforeIndex, + basePath.concat(elementIndex), + depth + 1 + ) + ); + } + } + + return wrappers; +} + +function staticObjectAssignArgumentSources(content, structure, sourceStart, sourceEnd) { + if (!structure.startsWith("Object", sourceStart)) return []; + + const assignAccess = staticStaticMemberPropertyAccessMatchAt(content, structure, sourceStart + "Object".length); + if (!assignAccess || assignAccess.value !== "assign") return []; + + const openParenIndex = skipWhitespaceForward(structure, assignAccess.end); + if (structure[openParenIndex] !== "(") return []; + + const closeParenIndex = findMatchingParen(structure, openParenIndex); + if (closeParenIndex === -1 || skipWhitespaceForward(structure, closeParenIndex + 1) !== sourceEnd) return []; + + const sources = []; + let argumentIndex = 1; + while (true) { + const argument = nthCallArgumentSource(content, structure, openParenIndex, closeParenIndex, argumentIndex); + if (!argument) break; + + sources.push(argument); + argumentIndex += 1; + } + + return sources; +} + +function staticObjectWrapperSources(content, structure, sourceMatcher) { + const wrappers = []; + const declarationPattern = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\b(?:\s*:[^=;]+)?\s*=/g; + + for (const declarationMatch of structure.matchAll(declarationPattern)) { + if (typeof declarationMatch.index !== "number") continue; + + const equalsIndex = structure.indexOf("=", declarationMatch.index); + if (equalsIndex === -1) continue; + + const initializerStart = skipWhitespaceForward(structure, equalsIndex + 1); + const statementEnd = findStatementEnd(structure, initializerStart); + const expressionEnd = skipWhitespaceBackward(structure, statementEnd - 1) + 1; + + for (const wrapper of staticWrapperSourcesInExpression( + content, + structure, + sourceMatcher, + initializerStart, + expressionEnd, + declarationMatch.index, + [], + 0 + )) { + if (wrapper.path.length === 0) continue; + wrappers.push({ + name: declarationMatch[1], + path: wrapper.path, + kind: "path", + index: declarationMatch.index, + end: statementEnd, + source: wrapper.source + }); + } + } + + return wrappers; +} + +function staticArrayWrapperSources(_content, _structure, _sourceMatcher) { + // 数组 wrapper 已由 staticObjectWrapperSources 的 path-source 递归统一覆盖。 + return []; +} + +function staticFunctionReturnWrapperSources(content, structure, sourceMatcher) { + const wrappers = []; + const functionPattern = /\bfunction\s+([A-Za-z_$][\w$]*)\s*\([^)]*\)\s*\{/g; + const declarationPattern = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\b(?:\s*:[^=;]+)?\s*=/g; + + for (const functionMatch of structure.matchAll(functionPattern)) { + if (typeof functionMatch.index !== "number") continue; + + const openBraceIndex = structure.indexOf("{", functionMatch.index); + const closeBraceIndex = openBraceIndex === -1 ? -1 : findMatchingBrace(structure, openBraceIndex); + if (closeBraceIndex === -1) continue; + + const bodyStart = openBraceIndex + 1; + const bodyEnd = closeBraceIndex; + const returnPattern = /\breturn\b/g; + returnPattern.lastIndex = bodyStart; + const returnMatch = returnPattern.exec(structure); + if (!returnMatch || returnMatch.index >= bodyEnd) continue; + + const expressionStart = skipWhitespaceForward(structure, returnMatch.index + returnMatch[0].length); + const statementEnd = Math.min(findStatementEnd(structure, expressionStart), bodyEnd); + wrappers.push( + ...staticFunctionReturnExpressionWrapperSources( + content, + structure, + sourceMatcher, + functionMatch[1], + functionMatch.index, + closeBraceIndex + 1, + expressionStart, + statementEnd + ) + ); + } + + for (const declarationMatch of structure.matchAll(declarationPattern)) { + if (typeof declarationMatch.index !== "number") continue; + + const equalsIndex = structure.indexOf("=", declarationMatch.index); + if (equalsIndex === -1) continue; + + const initializerStart = skipWhitespaceForward(structure, equalsIndex + 1); + const functionExpressionMatch = /^function\b\s*(?:[A-Za-z_$][\w$]*)?\s*\([^)]*\)\s*\{/.exec(structure.slice(initializerStart)); + if (functionExpressionMatch) { + const openBraceIndex = structure.indexOf("{", initializerStart + functionExpressionMatch[0].length - 1); + const closeBraceIndex = openBraceIndex === -1 ? -1 : findMatchingBrace(structure, openBraceIndex); + const statementEnd = closeBraceIndex === -1 ? -1 : findStatementEnd(structure, closeBraceIndex + 1); + if (closeBraceIndex !== -1 && statementEnd !== -1) { + const returnPattern = /\breturn\b/g; + returnPattern.lastIndex = openBraceIndex + 1; + const returnMatch = returnPattern.exec(structure); + if (returnMatch && returnMatch.index < closeBraceIndex) { + const expressionStart = skipWhitespaceForward(structure, returnMatch.index + returnMatch[0].length); + const expressionEnd = Math.min(findStatementEnd(structure, expressionStart), closeBraceIndex); + const aliasEnd = simpleStaticAliasInitializerEnd(structure, closeBraceIndex + 1, statementEnd); + if (aliasEnd !== -1) { + wrappers.push( + ...staticFunctionReturnExpressionWrapperSources( + content, + structure, + sourceMatcher, + declarationMatch[1], + declarationMatch.index, + aliasEnd, + expressionStart, + expressionEnd + ) + ); + } + } + } + continue; + } + + let statementEnd = findStatementEnd(structure, initializerStart); + const afterArrow = staticArrowExpressionBodyStart(structure, initializerStart, statementEnd); + if (afterArrow === -1) continue; + if (structure[afterArrow] === "{") { + const closeBraceIndex = findMatchingBrace(structure, afterArrow); + if (closeBraceIndex !== -1) statementEnd = findStatementEnd(structure, closeBraceIndex + 1); + const aliasEnd = closeBraceIndex === -1 ? -1 : simpleStaticAliasInitializerEnd(structure, closeBraceIndex + 1, statementEnd); + if (closeBraceIndex === -1 || aliasEnd === -1) continue; + + wrappers.push( + ...staticBlockReturnWrapperSources( + content, + structure, + sourceMatcher, + declarationMatch[1], + declarationMatch.index, + aliasEnd, + afterArrow + ) + ); + continue; + } + + const expressionEnd = skipWhitespaceBackward(structure, statementEnd - 1) + 1; + const aliasEnd = simpleStaticAliasInitializerEnd(structure, expressionEnd, statementEnd); + if (aliasEnd === -1) continue; + + wrappers.push( + ...staticFunctionReturnExpressionWrapperSources( + content, + structure, + sourceMatcher, + declarationMatch[1], + declarationMatch.index, + aliasEnd, + afterArrow, + expressionEnd + ) + ); + } + + return wrappers; +} + +function staticBlockReturnWrapperSources(content, structure, sourceMatcher, functionName, functionIndex, functionEnd, openBraceIndex) { + const closeBraceIndex = findMatchingBrace(structure, openBraceIndex); + if (closeBraceIndex === -1) return []; + + const bodyStart = openBraceIndex + 1; + const bodyEnd = closeBraceIndex; + const returnPattern = /\breturn\b/g; + returnPattern.lastIndex = bodyStart; + const returnMatch = returnPattern.exec(structure); + if (!returnMatch || returnMatch.index >= bodyEnd) return []; + + const nextReturn = returnPattern.exec(structure); + if (nextReturn && nextReturn.index < bodyEnd) return []; + + const expressionStart = skipWhitespaceForward(structure, returnMatch.index + returnMatch[0].length); + const statementEnd = Math.min(findStatementEnd(structure, expressionStart), bodyEnd); + return staticFunctionReturnExpressionWrapperSources( + content, + structure, + sourceMatcher, + functionName, + functionIndex, + functionEnd, + expressionStart, + statementEnd + ); +} + +function staticArrowExpressionBodyStart(structure, initializerStart, statementEnd) { + const arrowStart = skipWhitespaceForward(structure, initializerStart); + let cursor = arrowStart; + + if (structure[cursor] === "(") { + const paramsEnd = findMatchingParen(structure, cursor); + if (paramsEnd === -1 || paramsEnd >= statementEnd) return -1; + cursor = skipWhitespaceForward(structure, paramsEnd + 1); + } else { + const identifierMatch = /^[A-Za-z_$][\w$]*/.exec(structure.slice(cursor)); + if (!identifierMatch) return -1; + cursor = skipWhitespaceForward(structure, cursor + identifierMatch[0].length); + } + + if (structure[cursor] === ":") { + cursor = staticTypeAnnotationArrowIndex(structure, cursor + 1, statementEnd); + if (cursor === -1) return -1; + } + + cursor = skipWhitespaceForward(structure, cursor); + if (!structure.startsWith("=>", cursor)) return -1; + return skipWhitespaceForward(structure, cursor + 2); +} + +function staticTypeAnnotationArrowIndex(structure, sourceStart, sourceEnd) { + let parenDepth = 0; + let braceDepth = 0; + let bracketDepth = 0; + + for (let cursor = sourceStart; cursor < sourceEnd - 1; cursor += 1) { + const char = structure[cursor]; + if (char === "(") parenDepth += 1; + if (char === ")") parenDepth = Math.max(0, parenDepth - 1); + if (char === "{") braceDepth += 1; + if (char === "}") braceDepth = Math.max(0, braceDepth - 1); + if (char === "[") bracketDepth += 1; + if (char === "]") bracketDepth = Math.max(0, bracketDepth - 1); + + if (structure.startsWith("=>", cursor) && parenDepth === 0 && braceDepth === 0 && bracketDepth === 0) { + return cursor; + } + } + + return -1; +} + +function staticFunctionReturnExpressionWrapperSources( + content, + structure, + sourceMatcher, + functionName, + functionIndex, + functionEnd, + expressionStart, + expressionEnd +) { + // 函数返回 object/array 时保留内部静态 path,供 getBox().raw 与 const { raw } = getBox() 共用。 + return staticWrapperSourcesInExpression(content, structure, sourceMatcher, expressionStart, expressionEnd, functionIndex, [], 0).map( + (wrapper) => ({ + kind: wrapper.path.length === 0 ? "functionReturn" : "functionReturnPath", + name: functionName, + path: wrapper.path, + index: functionIndex, + end: functionEnd, + source: wrapper.source + }) + ); +} + +function staticWrapperSources(content, structure, sourceMatcher) { + return staticObjectWrapperSources(content, structure, sourceMatcher) + .concat(staticArrayWrapperSources(content, structure, sourceMatcher)) + .concat(staticFunctionReturnWrapperSources(content, structure, sourceMatcher)) + .toSorted((left, right) => left.end - right.end); +} + +function isStaticWrapperActiveAt(structure, wrapper, beforeIndex) { + if (wrapper.end >= beforeIndex) return false; + return !hasSimpleIdentifierAssignmentBetween(structure, wrapper.name, wrapper.end, beforeIndex); +} + +function staticArrayIndexAccessMatchAt(structure, index, expectedIndex) { + let bracketStart = skipWhitespaceForward(structure, index); + if (structure.startsWith("?.", bracketStart)) { + bracketStart = skipWhitespaceForward(structure, bracketStart + 2); + } + if (structure[bracketStart] !== "[") return null; + + const bracketEnd = findMatchingBracket(structure, bracketStart); + if (bracketEnd === -1) return null; + + const indexSource = structure.slice(bracketStart + 1, bracketEnd).trim(); + if (!/^\d+$/.test(indexSource) || Number.parseInt(indexSource, 10) !== expectedIndex) return null; + + return { index, end: bracketEnd + 1 }; +} + +function staticWrapperPathAccessMatchAt(content, structure, index, path) { + let cursor = index; + + for (const segment of path) { + const accessStart = skipWhitespaceForward(structure, cursor); + if (typeof segment === "number") { + const arrayAccess = staticArrayIndexAccessMatchAt(structure, accessStart, segment); + if (!arrayAccess) return null; + cursor = arrayAccess.end; + continue; + } + + const propertyAccess = staticPrismaPropertyAccessMatchAt(content, structure, accessStart, [segment], accessStart); + if (!propertyAccess) return null; + cursor = propertyAccess.end; + } + + return { index, end: cursor }; +} + +function staticWrapperSourceUsageMatchAt(content, structure, wrappers, index, beforeIndex = index) { + const usageStart = skipWhitespaceForward(structure, index); + const nameMatch = /^[A-Za-z_$][\w$]*/.exec(structure.slice(usageStart)); + if (!nameMatch || isMemberIdentifierReference(structure, usageStart)) return null; + + const name = nameMatch[0]; + const afterName = skipWhitespaceForward(structure, usageStart + name.length); + const candidates = wrappers.filter((wrapper) => wrapper.name === name && isStaticWrapperActiveAt(structure, wrapper, beforeIndex)); + + for (const wrapper of candidates.toSorted((left, right) => right.end - left.end)) { + if (wrapper.kind === "path") { + const pathAccess = staticWrapperPathAccessMatchAt(content, structure, afterName, wrapper.path); + if (pathAccess) return { index: usageStart, end: pathAccess.end, wrapper }; + continue; + } + + if (wrapper.kind === "functionReturn" || wrapper.kind === "functionReturnPath") { + const callEnd = staticZeroArgumentCallEndAt(content, structure, afterName); + if (callEnd === -1) continue; + + if (wrapper.kind === "functionReturn") return { index: usageStart, end: callEnd, wrapper }; + + const pathAccess = staticWrapperPathAccessMatchAt(content, structure, callEnd, wrapper.path); + if (pathAccess) return { index: usageStart, end: pathAccess.end, wrapper }; + } + } + + return null; +} + +function staticZeroArgumentCallEndAt(content, structure, index) { + let openParenIndex = skipWhitespaceForward(structure, index); + if (structure.startsWith("?.", openParenIndex)) { + openParenIndex = skipWhitespaceForward(structure, openParenIndex + 2); + } + if (structure[openParenIndex] !== "(") return -1; + + const closeParenIndex = findMatchingParen(structure, openParenIndex); + if (closeParenIndex === -1) return -1; + + return nthCallArgumentSource(content, structure, openParenIndex, closeParenIndex, 0) ? -1 : closeParenIndex + 1; +} + +function staticWrapperSourceUsageMatches(content, structure, wrappers) { + const matches = []; + const names = [...new Set(wrappers.map((wrapper) => wrapper.name))]; + if (names.length === 0) return matches; + + const namePattern = new RegExp(String.raw`\b(${names.map(escapeRegExp).join("|")})\b`, "g"); + for (const match of structure.matchAll(namePattern)) { + if (typeof match.index !== "number") continue; + + const usage = staticWrapperSourceUsageMatchAt(content, structure, wrappers, match.index, match.index); + if (usage) matches.push(usage); + } + + return matches.toSorted((left, right) => left.index - right.index); +} + +function findStaticWrapperUsageAliases(content, structure, wrappers) { + const aliases = []; + const declarationPattern = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\b(?:\s*:[^=;]+)?\s*=/g; + const assignmentPattern = /\b([A-Za-z_$][\w$]*)\b\s*=/g; + + for (const declarationMatch of structure.matchAll(declarationPattern)) { + if (typeof declarationMatch.index !== "number") continue; + + const equalsIndex = structure.indexOf("=", declarationMatch.index); + if (equalsIndex === -1) continue; + + const initializerStart = skipWhitespaceForward(structure, equalsIndex + 1); + const statementEnd = findStatementEnd(structure, initializerStart); + const usage = staticWrapperSourceUsageMatchAt(content, structure, wrappers, initializerStart, declarationMatch.index); + if (!usage) continue; + + const aliasEnd = simpleStaticAliasInitializerEnd(structure, usage.end, statementEnd); + if (aliasEnd === -1) continue; + + aliases.push({ name: declarationMatch[1], index: declarationMatch.index, end: aliasEnd }); + } + + for (const assignmentMatch of structure.matchAll(assignmentPattern)) { + if (typeof assignmentMatch.index !== "number") continue; + + const variableName = assignmentMatch[1]; + const variableIndex = assignmentMatch.index + assignmentMatch[0].indexOf(variableName); + if (isMemberIdentifierReference(structure, variableIndex)) continue; + if (/\b(?:const|let|var)\s*$/.test(structure.slice(Math.max(0, assignmentMatch.index - 16), variableIndex))) continue; + + const equalsIndex = structure.indexOf("=", assignmentMatch.index + assignmentMatch[0].lastIndexOf("=")); + if (equalsIndex === -1) continue; + if ("=>"].includes(structure[equalsIndex + 1] ?? "")) continue; + + const initializerStart = skipWhitespaceForward(structure, equalsIndex + 1); + const statementEnd = findStatementEnd(structure, initializerStart); + const usage = staticWrapperSourceUsageMatchAt(content, structure, wrappers, initializerStart, assignmentMatch.index); + if (!usage) continue; + + const aliasEnd = simpleStaticAliasInitializerEnd(structure, usage.end, statementEnd); + if (aliasEnd === -1) continue; + + aliases.push({ name: variableName, index: assignmentMatch.index, end: aliasEnd }); + } + + return aliases; +} + +function pathsEqual(left, right) { + return left.length === right.length && left.every((value, index) => value === right[index]); +} + +function activeWrapperForPath(structure, wrappers, objectName, path, beforeIndex) { + return wrappers + .filter( + (wrapper) => + wrapper.kind === "path" && + wrapper.name === objectName && + pathsEqual(wrapper.path, path) && + isStaticWrapperActiveAt(structure, wrapper, beforeIndex) + ) + .toSorted((left, right) => right.end - left.end)[0]; +} + +function activeFunctionReturnWrapperForPath(structure, wrappers, functionName, path, beforeIndex) { + return wrappers + .filter( + (wrapper) => + wrapper.kind === "functionReturnPath" && + wrapper.name === functionName && + pathsEqual(wrapper.path, path) && + isStaticWrapperActiveAt(structure, wrapper, beforeIndex) + ) + .toSorted((left, right) => right.end - left.end)[0]; +} + +function activeWrapperForDestructuringSource(content, structure, wrappers, sourceStart, path, beforeIndex) { + const nameMatch = /^[A-Za-z_$][\w$]*/.exec(structure.slice(sourceStart)); + if (!nameMatch || isMemberIdentifierReference(structure, sourceStart)) return null; + + const sourceName = nameMatch[0]; + const afterName = skipWhitespaceForward(structure, sourceStart + sourceName.length); + const callEnd = staticZeroArgumentCallEndAt(content, structure, afterName); + if (callEnd !== -1) { + const wrapper = activeFunctionReturnWrapperForPath(structure, wrappers, sourceName, path, beforeIndex); + return wrapper ? { wrapper, end: callEnd } : null; + } + + const wrapper = activeWrapperForPath(structure, wrappers, sourceName, path, beforeIndex); + return wrapper ? { wrapper, end: sourceStart + sourceName.length } : null; +} + +function destructuringAliasBindingsForWrapperPaths(content, structure, bindingStart, bindingEnd, beforeIndex, basePath = []) { + const bindings = []; + + for (const part of destructuringParts(structure, bindingStart, bindingEnd)) { + const propertyName = staticDestructuringPropertyKeyValue(content, structure, part.start, part.end, beforeIndex); + if (!propertyName) continue; + + const keyStart = skipWhitespaceForward(structure, part.start); + const keyEnd = destructuringPropertyKeyEnd(structure, keyStart, part.end); + if (keyEnd === -1) continue; + + const path = basePath.concat(propertyName); + const afterKey = skipWhitespaceForward(structure, keyEnd); + + if (structure[afterKey] === ":") { + const valueStart = skipWhitespaceForward(structure, afterKey + 1); + if (structure[valueStart] === "{") { + const nestedEnd = findMatchingBrace(structure, valueStart); + if (nestedEnd !== -1 && nestedEnd < part.end) { + bindings.push(...destructuringAliasBindingsForWrapperPaths(content, structure, valueStart + 1, nestedEnd, beforeIndex, path)); + } + continue; + } + + const aliasName = destructuringAliasName(structure, part.start, part.end); + if (aliasName) bindings.push({ aliasName, path }); + continue; + } + + const aliasName = destructuringAliasName(structure, part.start, part.end); + if (aliasName) bindings.push({ aliasName, path }); + } + + return bindings; +} + +function arrayDestructuringAliasBindingsForWrapperPaths(structure, bindingStart, bindingEnd, basePath = []) { + const bindings = []; + const parts = destructuringParts(structure, bindingStart, bindingEnd); + + for (const [elementIndex, part] of parts.entries()) { + const valueStart = skipWhitespaceForward(structure, part.start); + const valueEnd = skipWhitespaceBackward(structure, part.end - 1) + 1; + if (valueStart >= valueEnd) continue; + + const path = basePath.concat(elementIndex); + + if (structure[valueStart] === "[") { + const nestedEnd = findMatchingBracket(structure, valueStart); + if (nestedEnd !== -1 && nestedEnd === valueEnd - 1) { + bindings.push(...arrayDestructuringAliasBindingsForWrapperPaths(structure, valueStart + 1, nestedEnd, path)); + } + continue; + } + + const aliasMatch = /^[A-Za-z_$][\w$]*/.exec(structure.slice(valueStart, valueEnd)); + if (!aliasMatch) continue; + + const aliasEnd = valueStart + aliasMatch[0].length; + if (skipWhitespaceForward(structure, aliasEnd) !== valueEnd) continue; + + bindings.push({ aliasName: aliasMatch[0], path }); + } + + return bindings; +} + +function findStaticWrapperDestructuredAliases(content, structure, wrappers) { + const aliases = []; + const declarationPattern = /\b(?:const|let|var)\s*\{/g; + + for (const declarationMatch of structure.matchAll(declarationPattern)) { + if (typeof declarationMatch.index !== "number") continue; + + const openBraceIndex = structure.indexOf("{", declarationMatch.index); + if (openBraceIndex === -1) continue; + + const closeBraceIndex = findMatchingBrace(structure, openBraceIndex); + if (closeBraceIndex === -1) continue; + + let cursor = skipWhitespaceForward(structure, closeBraceIndex + 1); + if (structure[cursor] === ":") { + const equalsAfterType = structure.indexOf("=", cursor + 1); + const semicolonAfterType = structure.indexOf(";", cursor + 1); + if (equalsAfterType === -1 || (semicolonAfterType !== -1 && semicolonAfterType < equalsAfterType)) continue; + cursor = skipWhitespaceForward(structure, equalsAfterType + 1); + } else { + if (structure[cursor] !== "=") continue; + cursor = skipWhitespaceForward(structure, cursor + 1); + } + + const statementEnd = findStatementEnd(structure, cursor); + + for (const binding of destructuringAliasBindingsForWrapperPaths(content, structure, openBraceIndex + 1, closeBraceIndex, declarationMatch.index)) { + const source = activeWrapperForDestructuringSource(content, structure, wrappers, cursor, binding.path, declarationMatch.index); + if (!source) continue; + + const aliasEnd = simpleStaticAliasInitializerEnd(structure, source.end, statementEnd); + if (aliasEnd === -1) continue; + + aliases.push({ name: binding.aliasName, index: declarationMatch.index, end: aliasEnd }); + } + } + + return aliases; +} + +function findStaticWrapperArrayDestructuredAliases(content, structure, wrappers) { + const aliases = []; + const declarationPattern = /\b(?:const|let|var)\s*\[/g; + + for (const declarationMatch of structure.matchAll(declarationPattern)) { + if (typeof declarationMatch.index !== "number") continue; + + const openBracketIndex = structure.indexOf("[", declarationMatch.index); + if (openBracketIndex === -1) continue; + + const closeBracketIndex = findMatchingBracket(structure, openBracketIndex); + if (closeBracketIndex === -1) continue; + + let cursor = skipWhitespaceForward(structure, closeBracketIndex + 1); + if (structure[cursor] === ":") { + const equalsAfterType = structure.indexOf("=", cursor + 1); + const semicolonAfterType = structure.indexOf(";", cursor + 1); + if (equalsAfterType === -1 || (semicolonAfterType !== -1 && semicolonAfterType < equalsAfterType)) continue; + cursor = skipWhitespaceForward(structure, equalsAfterType + 1); + } else { + if (structure[cursor] !== "=") continue; + cursor = skipWhitespaceForward(structure, cursor + 1); + } + + const statementEnd = findStatementEnd(structure, cursor); + + for (const binding of arrayDestructuringAliasBindingsForWrapperPaths(structure, openBracketIndex + 1, closeBracketIndex)) { + const source = activeWrapperForDestructuringSource(content, structure, wrappers, cursor, binding.path, declarationMatch.index); + if (!source) continue; + + const aliasEnd = simpleStaticAliasInitializerEnd(structure, source.end, statementEnd); + if (aliasEnd === -1) continue; + + aliases.push({ name: binding.aliasName, index: declarationMatch.index, end: aliasEnd }); + } + } + + return aliases; +} + +function findStaticWrapperAssignedDestructuredAliases(content, structure, wrappers) { + const aliases = []; + const assignmentPattern = /\(\s*\{/g; + + for (const assignmentMatch of structure.matchAll(assignmentPattern)) { + if (typeof assignmentMatch.index !== "number") continue; + + const openParenIndex = structure.indexOf("(", assignmentMatch.index); + if (openParenIndex === -1 || !isParenthesizedExpressionOpen(structure, openParenIndex)) continue; + + const openBraceIndex = structure.indexOf("{", openParenIndex); + if (openBraceIndex === -1) continue; + + const closeBraceIndex = findMatchingBrace(structure, openBraceIndex); + const closeParenIndex = findMatchingParen(structure, openParenIndex); + if (closeBraceIndex === -1 || closeParenIndex === -1 || closeBraceIndex > closeParenIndex) continue; + + let cursor = skipWhitespaceForward(structure, closeBraceIndex + 1); + if (structure[cursor] !== "=") continue; + + cursor = skipWhitespaceForward(structure, cursor + 1); + const statementEnd = findStatementEnd(structure, openParenIndex); + + for (const binding of destructuringAliasBindingsForWrapperPaths(content, structure, openBraceIndex + 1, closeBraceIndex, assignmentMatch.index)) { + const source = activeWrapperForDestructuringSource(content, structure, wrappers, cursor, binding.path, assignmentMatch.index); + if (!source || skipWhitespaceForward(structure, source.end) !== closeParenIndex) continue; + + const aliasEnd = simpleStaticAliasInitializerEnd(structure, closeParenIndex + 1, statementEnd); + if (aliasEnd === -1) continue; + + aliases.push({ name: binding.aliasName, index: assignmentMatch.index, end: aliasEnd }); + } + } + + return aliases; +} + +function findStaticWrapperAssignedArrayDestructuredAliases(content, structure, wrappers) { + const aliases = []; + const assignmentPattern = /\(\s*\[/g; + + for (const assignmentMatch of structure.matchAll(assignmentPattern)) { + if (typeof assignmentMatch.index !== "number") continue; + + const openParenIndex = structure.indexOf("(", assignmentMatch.index); + if (openParenIndex === -1 || !isParenthesizedExpressionOpen(structure, openParenIndex)) continue; + + const openBracketIndex = structure.indexOf("[", openParenIndex); + if (openBracketIndex === -1) continue; + + const closeBracketIndex = findMatchingBracket(structure, openBracketIndex); + const closeParenIndex = findMatchingParen(structure, openParenIndex); + if (closeBracketIndex === -1 || closeParenIndex === -1 || closeBracketIndex > closeParenIndex) continue; + + let cursor = skipWhitespaceForward(structure, closeBracketIndex + 1); + if (structure[cursor] !== "=") continue; + + cursor = skipWhitespaceForward(structure, cursor + 1); + const statementEnd = findStatementEnd(structure, openParenIndex); + + for (const binding of arrayDestructuringAliasBindingsForWrapperPaths(structure, openBracketIndex + 1, closeBracketIndex)) { + const source = activeWrapperForDestructuringSource(content, structure, wrappers, cursor, binding.path, assignmentMatch.index); + if (!source || skipWhitespaceForward(structure, source.end) !== closeParenIndex) continue; + + const aliasEnd = simpleStaticAliasInitializerEnd(structure, closeParenIndex + 1, statementEnd); + if (aliasEnd === -1) continue; + + aliases.push({ name: binding.aliasName, index: assignmentMatch.index, end: aliasEnd }); + } + } + + return aliases; +} + +function findStaticWrapperExtractionAliases(content, structure, wrappers) { + return findStaticWrapperUsageAliases(content, structure, wrappers) + .concat(findStaticWrapperDestructuredAliases(content, structure, wrappers)) + .concat(findStaticWrapperArrayDestructuredAliases(content, structure, wrappers)) + .concat(findStaticWrapperAssignedDestructuredAliases(content, structure, wrappers)) + .concat(findStaticWrapperAssignedArrayDestructuredAliases(content, structure, wrappers)) + .toSorted((left, right) => left.end - right.end); +} + +function staticBuiltinInvokerAliasAt(structure, aliases, aliasName, beforeIndex) { + return latestStaticAliasRecordAt(structure, aliases, aliasName, beforeIndex); +} + +function staticBuiltinInvokerAliasKindAt(structure, aliases, aliasName, beforeIndex) { + return staticBuiltinInvokerAliasAt(structure, aliases, aliasName, beforeIndex)?.kind ?? null; +} + +function aliasDirectInvocationCallMatches(structure, aliasNames) { + const uniqueAliasNames = [...new Set(aliasNames)]; + if (uniqueAliasNames.length === 0) return []; + + const matches = []; + const aliasPattern = new RegExp(String.raw`\b(${uniqueAliasNames.map(escapeRegExp).join("|")})\b`, "g"); + + for (const match of structure.matchAll(aliasPattern)) { + if (typeof match.index !== "number") continue; + + const aliasName = match[1]; + const aliasIndex = match.index + match[0].indexOf(aliasName); + if (isMemberIdentifierReference(structure, aliasIndex)) continue; + + let openParenIndex = skipWhitespaceForward(structure, aliasIndex + aliasName.length); + if (structure.startsWith("?.", openParenIndex)) { + openParenIndex = skipWhitespaceForward(structure, openParenIndex + 2); + } + if (structure[openParenIndex] !== "(") continue; + + const closeParenIndex = findMatchingParen(structure, openParenIndex); + if (closeParenIndex === -1) continue; + + matches.push({ aliasName, aliasIndex, openParenIndex, closeParenIndex }); + } + + return matches; +} + +function functionPrototypeMethodKindFromArgument(content, structure, argument, aliases) { + if (!argument) return null; + + const directKind = functionPrototypeMethodKind(structure, argument.index, argument.end); + if (directKind) return directKind; + + const aliasName = simpleIdentifierSourceName(structure, argument.index, argument.end); + if (!aliasName) return null; + + const aliasKind = staticBuiltinInvokerAliasKindAt(structure, aliases, aliasName, argument.index); + return aliasKind === "call" || aliasKind === "apply" ? aliasKind : null; +} + +function functionPrototypeCallInvocationMatches(content, structure, aliases) { + const matches = []; + const directCallPattern = /\bFunction\b/g; + + for (const match of structure.matchAll(directCallPattern)) { + if (typeof match.index !== "number") continue; + + const baseMatch = staticBuiltinInvokerBaseExpressionMatchAtWithContent(content, structure, match.index); + if (!baseMatch || (baseMatch.kind !== "call" && baseMatch.kind !== "apply")) continue; + + const operation = staticBuiltinMemberOperationMatchAt(content, structure, baseMatch.end, ["call", "apply"]); + if (!operation) continue; + + const openParenIndex = skipWhitespaceForward(structure, operation.end); + if (structure[openParenIndex] !== "(") continue; + + const closeParenIndex = openParenIndex === -1 ? -1 : findMatchingParen(structure, openParenIndex); + if (closeParenIndex === -1) continue; + + matches.push({ index: match.index, methodKind: baseMatch.kind, mode: operation.value, openParenIndex, closeParenIndex }); + } + + const aliasNames = [...new Set(aliases.filter((alias) => (alias.kind === "call" || alias.kind === "apply") && !alias.bound).map((alias) => alias.name))]; + if (aliasNames.length > 0) { + const aliasCallPattern = new RegExp(String.raw`\b(${aliasNames.map(escapeRegExp).join("|")})\b`, "g"); + for (const match of structure.matchAll(aliasCallPattern)) { + if (typeof match.index !== "number") continue; + + const aliasName = match[1]; + const aliasIndex = match.index + match[0].indexOf(aliasName); + if (isMemberIdentifierReference(structure, aliasIndex)) continue; + + const aliasKind = staticBuiltinInvokerAliasKindAt(structure, aliases, aliasName, aliasIndex); + if (aliasKind !== "call" && aliasKind !== "apply") continue; + + const operation = staticBuiltinMemberOperationMatchAt(content, structure, aliasIndex + aliasName.length, ["call", "apply"]); + if (!operation) continue; + + const openParenIndex = skipWhitespaceForward(structure, operation.end); + if (structure[openParenIndex] !== "(") continue; + + const closeParenIndex = openParenIndex === -1 ? -1 : findMatchingParen(structure, openParenIndex); + if (closeParenIndex === -1) continue; + + matches.push({ index: aliasIndex, methodKind: aliasKind, mode: operation.value, openParenIndex, closeParenIndex }); + } + } + + const boundAliasNames = [...new Set(aliases.filter((alias) => (alias.kind === "call" || alias.kind === "apply") && alias.bound).map((alias) => alias.name))]; + if (boundAliasNames.length === 0) return matches.toSorted((left, right) => left.index - right.index); + + for (const match of aliasDirectInvocationCallMatches(structure, boundAliasNames)) { + const { aliasName, aliasIndex, openParenIndex, closeParenIndex } = match; + const alias = staticBuiltinInvokerAliasAt(structure, aliases, aliasName, aliasIndex); + if (!alias?.bound || (alias.kind !== "call" && alias.kind !== "apply")) continue; + + matches.push({ index: aliasIndex, methodKind: alias.kind, mode: "call", openParenIndex, closeParenIndex }); + } + + return matches.toSorted((left, right) => left.index - right.index); +} + +function functionPrototypeEquivalentArgumentSource(content, structure, invocation, argumentIndex) { + if (invocation.mode === "call") { + return nthCallArgumentSource(content, structure, invocation.openParenIndex, invocation.closeParenIndex, argumentIndex); + } + + if (invocation.mode === "apply") { + if (argumentIndex === 0) { + return nthCallArgumentSource(content, structure, invocation.openParenIndex, invocation.closeParenIndex, 0); + } + + const arrayArgument = nthCallArgumentSource(content, structure, invocation.openParenIndex, invocation.closeParenIndex, 1); + return nthArrayElementSource(content, structure, arrayArgument, argumentIndex - 1); + } + + return null; +} + +function functionPrototypeBoundTargetExpressionMatchAt(content, structure, index) { + const expressionStart = skipWhitespaceForward(structure, index); + const baseMatch = staticBuiltinInvokerBaseExpressionMatchAtWithContent(content, structure, expressionStart); + if (!baseMatch || (baseMatch.kind !== "call" && baseMatch.kind !== "apply")) return null; + + const bindMethodStart = skipWhitespaceForward(structure, baseMatch.end); + const bindMethodMatch = staticBuiltinMemberOperationMatchAt(content, structure, bindMethodStart, ["bind"]); + if (!bindMethodMatch) return null; + + const bindCallStart = skipWhitespaceForward(structure, bindMethodMatch.end); + if (structure[bindCallStart] !== "(") return null; + + const bindCallEnd = findMatchingParen(structure, bindCallStart); + if (bindCallEnd === -1) return null; + + const targetArgument = nthCallArgumentSource(content, structure, bindCallStart, bindCallEnd, 0); + if (!targetArgument) return null; + + return { methodKind: baseMatch.kind, targetArgument, end: bindCallEnd + 1 }; +} + +function findFunctionPrototypeBoundTargetAliases(content, structure) { + const aliases = []; + const declarationPattern = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\b(?:\s*:[^=;]+)?\s*=/g; + const assignmentPattern = /\b([A-Za-z_$][\w$]*)\b\s*=/g; + + for (const declarationMatch of structure.matchAll(declarationPattern)) { + if (typeof declarationMatch.index !== "number") continue; + + const equalsIndex = structure.indexOf("=", declarationMatch.index); + if (equalsIndex === -1) continue; + + const initializerStart = skipWhitespaceForward(structure, equalsIndex + 1); + const statementEnd = findStatementEnd(structure, initializerStart); + const boundTargetMatch = functionPrototypeBoundTargetExpressionMatchAt(content, structure, initializerStart); + if (!boundTargetMatch) continue; + + const aliasEnd = simpleStaticAliasInitializerEnd(structure, boundTargetMatch.end, statementEnd); + if (aliasEnd === -1) continue; + + // 绑定 forbidden target 的 wrapper alias 只跟踪简单同文件声明,复杂重赋值保持未知。 + aliases.push({ + name: declarationMatch[1], + methodKind: boundTargetMatch.methodKind, + targetArgument: boundTargetMatch.targetArgument, + index: declarationMatch.index, + end: aliasEnd + }); + } + + for (const assignmentMatch of structure.matchAll(assignmentPattern)) { + if (typeof assignmentMatch.index !== "number") continue; + + const variableName = assignmentMatch[1]; + const variableIndex = assignmentMatch.index + assignmentMatch[0].indexOf(variableName); + if (isMemberIdentifierReference(structure, variableIndex)) continue; + if (/\b(?:const|let|var)\s*$/.test(structure.slice(Math.max(0, assignmentMatch.index - 16), variableIndex))) continue; + + const equalsIndex = structure.indexOf("=", assignmentMatch.index + assignmentMatch[0].lastIndexOf("=")); + if (equalsIndex === -1) continue; + if ("=>"].includes(structure[equalsIndex + 1] ?? "")) continue; + + const initializerStart = skipWhitespaceForward(structure, equalsIndex + 1); + const statementEnd = findStatementEnd(structure, initializerStart); + const boundTargetMatch = functionPrototypeBoundTargetExpressionMatchAt(content, structure, initializerStart); + if (!boundTargetMatch) continue; + + const aliasEnd = simpleStaticAliasInitializerEnd(structure, boundTargetMatch.end, statementEnd); + if (aliasEnd === -1) continue; + + aliases.push({ + name: variableName, + methodKind: boundTargetMatch.methodKind, + targetArgument: boundTargetMatch.targetArgument, + index: assignmentMatch.index, + end: aliasEnd + }); + } + + return propagateStaticIdentifierAliases(structure, aliases, (sourceAlias, initializer) => ({ + name: initializer.name, + methodKind: sourceAlias.methodKind, + targetArgument: sourceAlias.targetArgument, + index: initializer.index, + end: initializer.end + })); +} + +function functionPrototypeBoundTargetAliasAt(structure, aliases, aliasName, beforeIndex) { + return latestStaticAliasRecordAt(structure, aliases, aliasName, beforeIndex); +} + +function functionPrototypeBoundTargetInvocationMatches(content, structure, aliases) { + const matches = []; + const directPattern = /\bFunction\b/g; + + for (const match of structure.matchAll(directPattern)) { + if (typeof match.index !== "number") continue; + + const boundTargetMatch = functionPrototypeBoundTargetExpressionMatchAt(content, structure, match.index); + if (!boundTargetMatch) continue; + + const openParenIndex = skipWhitespaceForward(structure, boundTargetMatch.end); + if (structure[openParenIndex] !== "(") continue; + + const closeParenIndex = findMatchingParen(structure, openParenIndex); + if (closeParenIndex === -1) continue; + + matches.push({ + index: match.index, + methodKind: boundTargetMatch.methodKind, + targetArgument: boundTargetMatch.targetArgument, + openParenIndex, + closeParenIndex + }); + } + + const aliasNames = [...new Set(aliases.map((alias) => alias.name))]; + if (aliasNames.length === 0) return matches; + + for (const match of aliasDirectInvocationCallMatches(structure, aliasNames)) { + const { aliasName, aliasIndex, openParenIndex, closeParenIndex } = match; + const alias = functionPrototypeBoundTargetAliasAt(structure, aliases, aliasName, aliasIndex); + if (!alias) continue; + + matches.push({ + index: aliasIndex, + methodKind: alias.methodKind, + targetArgument: alias.targetArgument, + openParenIndex, + closeParenIndex + }); + } + + return matches.toSorted((left, right) => left.index - right.index); +} + +function reflectApplyInvocationMatches(content, structure, aliases) { + const matches = []; + const directReflectApplyPattern = /\bReflect\b/g; + + for (const match of structure.matchAll(directReflectApplyPattern)) { + if (typeof match.index !== "number") continue; + + const baseMatch = staticBuiltinInvokerBaseExpressionMatchAtWithContent(content, structure, match.index); + if (!baseMatch || baseMatch.kind !== "reflectApply") continue; + + const operation = staticBuiltinMemberOperationMatchAt(content, structure, baseMatch.end, ["call", "apply"]); + if (operation) { + const openParenIndex = skipWhitespaceForward(structure, operation.end); + if (structure[openParenIndex] !== "(") continue; + + const closeParenIndex = findMatchingParen(structure, openParenIndex); + if (closeParenIndex === -1) continue; + + matches.push({ index: match.index, mode: operation.value, openParenIndex, closeParenIndex }); + continue; + } + + const openParenIndex = skipWhitespaceForward(structure, baseMatch.end); + if (structure[openParenIndex] !== "(") continue; + + const closeParenIndex = findMatchingParen(structure, openParenIndex); + if (closeParenIndex === -1) continue; + + matches.push({ index: match.index, mode: "direct", openParenIndex, closeParenIndex }); + } + + const aliasNames = [...new Set(aliases.filter((alias) => alias.kind === "reflectApply").map((alias) => alias.name))]; + if (aliasNames.length === 0) return matches; + + const secondOrderAliasNames = [...new Set(aliases.filter((alias) => alias.kind === "reflectApply" && !alias.bound).map((alias) => alias.name))]; + const aliasSecondOrderPattern = + secondOrderAliasNames.length > 0 + ? new RegExp(String.raw`\b(${secondOrderAliasNames.map(escapeRegExp).join("|")})\b\s*(?:\.|\?\.)\s*(call|apply)\s*\(`, "g") + : null; + for (const match of aliasDirectInvocationCallMatches(structure, aliasNames)) { + const { aliasName, aliasIndex, openParenIndex, closeParenIndex } = match; + const aliasKind = staticBuiltinInvokerAliasKindAt(structure, aliases, aliasName, aliasIndex); + if (aliasKind !== "reflectApply") continue; + + matches.push({ index: aliasIndex, mode: "direct", openParenIndex, closeParenIndex }); + } + + if (aliasSecondOrderPattern) { + const aliasSecondOrderNamePattern = new RegExp(String.raw`\b(${secondOrderAliasNames.map(escapeRegExp).join("|")})\b`, "g"); + for (const match of structure.matchAll(aliasSecondOrderNamePattern)) { + if (typeof match.index !== "number") continue; + + const aliasName = match[1]; + const aliasIndex = match.index + match[0].indexOf(aliasName); + if (isMemberIdentifierReference(structure, aliasIndex)) continue; + + const aliasKind = staticBuiltinInvokerAliasKindAt(structure, aliases, aliasName, aliasIndex); + if (aliasKind !== "reflectApply") continue; + + const operation = staticBuiltinMemberOperationMatchAt(content, structure, aliasIndex + aliasName.length, ["call", "apply"]); + if (!operation) continue; + + const openParenIndex = skipWhitespaceForward(structure, operation.end); + if (structure[openParenIndex] !== "(") continue; + + const closeParenIndex = openParenIndex === -1 ? -1 : findMatchingParen(structure, openParenIndex); + if (closeParenIndex === -1) continue; + + matches.push({ index: aliasIndex, mode: operation.value, openParenIndex, closeParenIndex }); + } + } + + return matches.toSorted((left, right) => left.index - right.index); +} + +function reflectApplyEquivalentArgumentSource(content, structure, invocation, argumentIndex) { + if (invocation.mode === "direct") { + return nthCallArgumentSource(content, structure, invocation.openParenIndex, invocation.closeParenIndex, argumentIndex); + } + + if (invocation.mode === "call") { + return nthCallArgumentSource(content, structure, invocation.openParenIndex, invocation.closeParenIndex, argumentIndex + 1); + } + + if (invocation.mode === "apply") { + const arrayArgument = nthCallArgumentSource(content, structure, invocation.openParenIndex, invocation.closeParenIndex, 1); + return nthArrayElementSource(content, structure, arrayArgument, argumentIndex); + } + + return null; +} + function prismaModelMethodCallPattern(models, methods) { return new RegExp( `${staticPrismaPropertyAccessPattern(models)}\\s*${staticPrismaPropertyAccessPattern(methods)}\\s*(?:\\?\\.)?\\s*\\(`, @@ -338,55 +2355,1025 @@ function prismaModelMethodCallPattern(models, methods) { ); } -function findStaticPrismaDelegateAliases(structure, models) { +function findStaticPrismaDelegateAliases(content, structure, models) { const aliases = []; - const modelAccess = staticPrismaPropertyAccessPattern(models); const declarationPattern = new RegExp( - String.raw`\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\b(?:\s*:[^=;]+)?\s*=\s*[A-Za-z_$][\w$]*\s*${modelAccess}`, + String.raw`\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\b(?:\s*:[^=;]+)?\s*=`, "g" ); for (const match of structure.matchAll(declarationPattern)) { if (typeof match.index !== "number") continue; - const aliasEnd = match.index + match[0].length; - const nextIndex = skipWhitespaceForward(structure, aliasEnd); + const equalsIndex = structure.indexOf("=", match.index); + if (equalsIndex === -1) continue; + + const initializerStart = skipWhitespaceForward(structure, equalsIndex + 1); + const statementEnd = findStatementEnd(structure, initializerStart); + const branchDelegateMatch = staticInitializerBranchMatch( + content, + structure, + initializerStart, + statementEnd, + (scanContent, scanStructure, sourceStart, sourceEnd) => + completeStaticPrismaDelegateAliasExpressionMatch(scanContent, scanStructure, sourceStart, sourceEnd, models) + ); + if (branchDelegateMatch?.branch) { + // 条件/逻辑分支里只要出现受保护 Prisma delegate,后续 alias.method(...) 必须按真实 delegate 写入扫描。 + aliases.push({ name: match[1], index: match.index, end: statementEnd }); + continue; + } + + const delegateMatch = staticPrismaDelegateExpressionMatchAt(content, structure, initializerStart, models); + if (!delegateMatch) continue; + + const nextIndex = skipWhitespaceForward(structure, delegateMatch.end); // 只接受 `const jobs = db.job;` 这类完整 delegate alias,避免把 `db.job.update` 误判成 alias。 if (structure[nextIndex] === "." || structure[nextIndex] === "[" || structure[nextIndex] === "(" || structure[nextIndex] === "?") { continue; } - aliases.push({ name: match[1], index: match.index }); + const aliasEnd = simpleStaticAliasInitializerEnd(structure, delegateMatch.end, statementEnd); + if (aliasEnd === -1) continue; + + aliases.push({ name: match[1], index: match.index, end: aliasEnd }); + } + + aliases.push(...findStaticPrismaDestructuredDelegateAliases(content, structure, models)); + aliases.push(...findStaticPrismaAssignedDestructuredDelegateAliases(content, structure, models)); + aliases.push(...findStaticPrismaAssignedDelegateAliases(content, structure, models)); + + const directAliases = propagateStaticIdentifierAliases(structure, aliases, (_sourceAlias, initializer) => ({ + name: initializer.name, + index: initializer.index, + end: initializer.end + })); + + const wrappers = prismaDelegateWrapperSources(content, structure, models, directAliases); + directAliases.push(...findStaticWrapperExtractionAliases(content, structure, wrappers)); + + return propagateStaticIdentifierAliases(structure, directAliases, (_sourceAlias, initializer) => ({ + name: initializer.name, + index: initializer.index, + end: initializer.end + })); +} + +function findStaticPrismaDestructuredDelegateAliases(content, structure, models) { + const aliases = []; + const declarationPattern = /\b(?:const|let|var)\s*\{/g; + + for (const declarationMatch of structure.matchAll(declarationPattern)) { + if (typeof declarationMatch.index !== "number") continue; + + const openBraceIndex = structure.indexOf("{", declarationMatch.index); + if (openBraceIndex === -1) continue; + const closeBraceIndex = findMatchingBrace(structure, openBraceIndex); + if (closeBraceIndex === -1) continue; + + const names = destructuredAliasNamesForStaticKeys(content, structure, openBraceIndex + 1, closeBraceIndex, models, declarationMatch.index); + if (names.length === 0) continue; + + let cursor = skipWhitespaceForward(structure, closeBraceIndex + 1); + if (structure[cursor] === ":") { + const equalsAfterType = structure.indexOf("=", cursor + 1); + const semicolonAfterType = structure.indexOf(";", cursor + 1); + if (equalsAfterType === -1 || (semicolonAfterType !== -1 && semicolonAfterType < equalsAfterType)) continue; + cursor = skipWhitespaceForward(structure, equalsAfterType + 1); + } else { + if (structure[cursor] !== "=") continue; + cursor = skipWhitespaceForward(structure, cursor + 1); + } + + const receiverMatch = simpleStaticMemberChainMatchAt(content, structure, cursor); + if (!receiverMatch) continue; + + // 解构 alias 只跟踪静态 Prisma client receiver,避免把运行时对象结构误判成 delegate。 + const expressionEnd = cursor + receiverMatch[0].length; + const statementEnd = findStatementEnd(structure, cursor); + const aliasEnd = simpleStaticAliasInitializerEnd(structure, expressionEnd, statementEnd); + if (aliasEnd === -1) continue; + + for (const name of names) { + aliases.push({ name, index: declarationMatch.index, end: aliasEnd }); + } } return aliases; } +function findStaticPrismaAssignedDestructuredDelegateAliases(content, structure, models) { + const aliases = []; + const assignmentPattern = /\(\s*\{/g; + + for (const assignmentMatch of structure.matchAll(assignmentPattern)) { + if (typeof assignmentMatch.index !== "number") continue; + + const openParenIndex = structure.indexOf("(", assignmentMatch.index); + if (openParenIndex === -1 || !isParenthesizedExpressionOpen(structure, openParenIndex)) continue; + + const openBraceIndex = structure.indexOf("{", openParenIndex); + if (openBraceIndex === -1) continue; + + const closeBraceIndex = findMatchingBrace(structure, openBraceIndex); + const closeParenIndex = findMatchingParen(structure, openParenIndex); + if (closeBraceIndex === -1 || closeParenIndex === -1 || closeBraceIndex > closeParenIndex) continue; + + const names = destructuredAliasNamesForStaticKeys(content, structure, openBraceIndex + 1, closeBraceIndex, models, assignmentMatch.index); + if (names.length === 0) continue; + + let cursor = skipWhitespaceForward(structure, closeBraceIndex + 1); + if (structure[cursor] !== "=") continue; + + cursor = skipWhitespaceForward(structure, cursor + 1); + const receiverMatch = simpleStaticMemberChainMatchAt(content, structure, cursor); + if (!receiverMatch) continue; + + const expressionEnd = cursor + receiverMatch[0].length; + if (skipWhitespaceForward(structure, expressionEnd) !== closeParenIndex) continue; + + // 赋值式解构 alias 覆盖 `let versions; ({ gameVersion: versions } = db)`。 + const statementEnd = findStatementEnd(structure, openParenIndex); + const aliasEnd = simpleStaticAliasInitializerEnd(structure, closeParenIndex + 1, statementEnd); + if (aliasEnd === -1) continue; + + for (const name of names) { + aliases.push({ name, index: assignmentMatch.index, end: aliasEnd }); + } + } + + return aliases; +} + +function isStaticAliasExpressionContinuation(structure, index) { + if (index >= structure.length) return false; + if (structure.startsWith("?.", index)) return true; + + return "([.`+-/*%,?:&|<>=!".includes(structure[index]); +} + +function simpleStaticAliasInitializerEnd(structure, expressionEnd, statementEnd) { + const nextTokenIndex = skipWhitespaceForward(structure, expressionEnd); + if (nextTokenIndex === statementEnd) return statementEnd; + + const gapAfterExpression = structure.slice(expressionEnd, nextTokenIndex); + if (!hasLineTerminator(gapAfterExpression)) return -1; + if (isStaticAliasExpressionContinuation(structure, nextTokenIndex)) return -1; + + return expressionEnd; +} + +function completeStaticPrismaDelegateAliasExpressionMatch(content, structure, sourceStart, sourceEnd, models) { + const unwrapped = unwrapCompleteParenthesizedJavaScriptExpression(structure, sourceStart, sourceEnd); + const expressionStart = skipWhitespaceForward(structure, unwrapped.start); + const expressionEnd = skipWhitespaceBackward(structure, unwrapped.end - 1) + 1; + if (expressionStart >= expressionEnd) return null; + + const delegateMatch = staticPrismaDelegateExpressionMatchAt(content, structure, expressionStart, models); + if (!delegateMatch || delegateMatch.end > expressionEnd) return null; + if (!isAtStaticWrapperExpressionEnd(structure, delegateMatch.end, expressionEnd)) return null; + + return { index: expressionStart, end: expressionEnd }; +} + +function findStaticPrismaAssignedDelegateAliases(content, structure, models) { + const aliases = []; + const assignmentPattern = /\b([A-Za-z_$][\w$]*)\b\s*=/g; + + for (const assignmentMatch of structure.matchAll(assignmentPattern)) { + if (typeof assignmentMatch.index !== "number") continue; + if (isMemberIdentifierReference(structure, assignmentMatch.index)) continue; + + const equalsIndex = structure.indexOf("=", assignmentMatch.index + assignmentMatch[0].lastIndexOf("=")); + if (equalsIndex === -1) continue; + if ("=>"].includes(structure[equalsIndex + 1] ?? "")) continue; + + const initializerStart = skipWhitespaceForward(structure, equalsIndex + 1); + const statementEnd = findStatementEnd(structure, initializerStart); + const branchDelegateMatch = staticInitializerBranchMatch( + content, + structure, + initializerStart, + statementEnd, + (scanContent, scanStructure, sourceStart, sourceEnd) => + completeStaticPrismaDelegateAliasExpressionMatch(scanContent, scanStructure, sourceStart, sourceEnd, models) + ); + if (branchDelegateMatch?.branch) { + // 后续赋值同样按 fail-closed 处理 mixed ternary/logical delegate source。 + aliases.push({ name: assignmentMatch[1], index: assignmentMatch.index, end: statementEnd }); + continue; + } + + const delegateMatch = staticPrismaDelegateExpressionMatchAt(content, structure, initializerStart, models); + if (!delegateMatch) continue; + if (delegateMatch.end > statementEnd) continue; + + const nextIndex = skipWhitespaceForward(structure, delegateMatch.end); + if (structure[nextIndex] === "." || structure[nextIndex] === "[" || structure[nextIndex] === "(" || structure[nextIndex] === "?") { + continue; + } + + const aliasEnd = simpleStaticAliasInitializerEnd(structure, delegateMatch.end, statementEnd); + if (aliasEnd === -1) continue; + + // 后置赋值 alias 只接受 `jobs = db.job` 这种完整 delegate,避免比较、箭头或复杂表达式误判。 + aliases.push({ name: assignmentMatch[1], index: assignmentMatch.index, end: aliasEnd }); + } + + return aliases; +} + +function prismaDelegateSourceMatcher(models, aliasResolver = null) { + return (content, structure, sourceStart, sourceEnd) => { + const delegateMatch = staticPrismaDelegateExpressionMatchAt(content, structure, sourceStart, models); + if (delegateMatch && isAtStaticWrapperExpressionEnd(structure, delegateMatch.end, sourceEnd)) return delegateMatch; + + return aliasResolver ? aliasResolver(structure, sourceStart, sourceEnd) : null; + }; +} + +function prismaDelegateWrapperSources(content, structure, models, aliases = []) { + const aliasResolver = (scanStructure, sourceStart, sourceEnd) => + staticIdentifierAliasSourceMatchAt(scanStructure, aliases, sourceStart, sourceEnd, sourceStart); + return staticWrapperSources(content, structure, prismaDelegateSourceMatcher(models, aliasResolver)); +} + +function prismaModelMethodSourceMatcher(models, methods, aliases, methodAliases = []) { + return (content, structure, sourceStart, sourceEnd) => { + const methodAccess = staticPrismaModelMethodAccessMatchAt(content, structure, sourceStart, models, methods, aliases); + if (methodAccess) { + if (isAtStaticWrapperExpressionEnd(structure, methodAccess.end, sourceEnd)) return methodAccess; + + const bindCloseParenIndex = prismaMethodBindCloseParenIndex(content, structure, methodAccess.end, sourceEnd); + // 函数返回或 Object.assign wrapper 里出现 `db.model.method.bind(db.model)` 时,后续调用仍等价于直接 Prisma 写。 + if (bindCloseParenIndex !== -1 && isAtStaticWrapperExpressionEnd(structure, bindCloseParenIndex + 1, sourceEnd)) { + return { index: methodAccess.index, end: bindCloseParenIndex + 1 }; + } + } + + return staticIdentifierAliasSourceMatchAt(structure, methodAliases, sourceStart, sourceEnd, sourceStart); + }; +} + +function prismaModelMethodWrapperSources(content, structure, models, methods, aliases, methodAliases = []) { + return staticWrapperSources(content, structure, prismaModelMethodSourceMatcher(models, methods, aliases, methodAliases)); +} + +function prismaModelMethodWrapperAliases(content, structure, methodWrappers) { + return propagateStaticIdentifierAliases( + structure, + findStaticWrapperExtractionAliases(content, structure, methodWrappers), + (_sourceAlias, initializer) => ({ + name: initializer.name, + index: initializer.index, + end: initializer.end + }) + ); +} + +function staticPrismaDelegateExpressionOrAliasMatchAt(content, structure, index, models, aliases = []) { + const delegateMatch = staticPrismaDelegateExpressionMatchAt(content, structure, index, models); + if (delegateMatch) return delegateMatch; + + const aliasMatch = /^[A-Za-z_$][\w$]*/.exec(structure.slice(index)); + if (!aliasMatch || isMemberIdentifierReference(structure, index)) return null; + + const aliasName = aliasMatch[0]; + if (!latestStaticAliasRecordAt(structure, aliases, aliasName, index)) return null; + + return { index, end: index + aliasName.length }; +} + +function findStaticPrismaDestructuredModelMethodAliases(content, structure, models, methods, aliases) { + const methodAliases = []; + const declarationPattern = /\b(?:const|let|var)\s*\{/g; + + for (const declarationMatch of structure.matchAll(declarationPattern)) { + if (typeof declarationMatch.index !== "number") continue; + + const openBraceIndex = structure.indexOf("{", declarationMatch.index); + if (openBraceIndex === -1) continue; + const closeBraceIndex = findMatchingBrace(structure, openBraceIndex); + if (closeBraceIndex === -1) continue; + + const names = destructuredAliasNamesForStaticKeys(content, structure, openBraceIndex + 1, closeBraceIndex, methods, declarationMatch.index); + if (names.length === 0) continue; + + let cursor = skipWhitespaceForward(structure, closeBraceIndex + 1); + if (structure[cursor] === ":") { + const equalsAfterType = structure.indexOf("=", cursor + 1); + const semicolonAfterType = structure.indexOf(";", cursor + 1); + if (equalsAfterType === -1 || (semicolonAfterType !== -1 && semicolonAfterType < equalsAfterType)) continue; + cursor = skipWhitespaceForward(structure, equalsAfterType + 1); + } else { + if (structure[cursor] !== "=") continue; + cursor = skipWhitespaceForward(structure, cursor + 1); + } + + const delegateMatch = staticPrismaDelegateExpressionOrAliasMatchAt(content, structure, cursor, models, aliases); + if (!delegateMatch) continue; + + const statementEnd = findStatementEnd(structure, cursor); + const aliasEnd = simpleStaticAliasInitializerEnd(structure, delegateMatch.end, statementEnd); + if (aliasEnd === -1) continue; + + // Prisma delegate method destructuring,例如 `const { delete: del } = db.auditLog`。 + for (const name of names) { + methodAliases.push({ name, index: declarationMatch.index, end: aliasEnd }); + } + } + + return methodAliases; +} + +function findStaticPrismaAssignedDestructuredModelMethodAliases(content, structure, models, methods, aliases) { + const methodAliases = []; + const assignmentPattern = /\(\s*\{/g; + + for (const assignmentMatch of structure.matchAll(assignmentPattern)) { + if (typeof assignmentMatch.index !== "number") continue; + + const openParenIndex = structure.indexOf("(", assignmentMatch.index); + if (openParenIndex === -1 || !isParenthesizedExpressionOpen(structure, openParenIndex)) continue; + + const openBraceIndex = structure.indexOf("{", openParenIndex); + if (openBraceIndex === -1) continue; + + const closeBraceIndex = findMatchingBrace(structure, openBraceIndex); + const closeParenIndex = findMatchingParen(structure, openParenIndex); + if (closeBraceIndex === -1 || closeParenIndex === -1 || closeBraceIndex > closeParenIndex) continue; + + const names = destructuredAliasNamesForStaticKeys(content, structure, openBraceIndex + 1, closeBraceIndex, methods, assignmentMatch.index); + if (names.length === 0) continue; + + let cursor = skipWhitespaceForward(structure, closeBraceIndex + 1); + if (structure[cursor] !== "=") continue; + + cursor = skipWhitespaceForward(structure, cursor + 1); + const delegateMatch = staticPrismaDelegateExpressionOrAliasMatchAt(content, structure, cursor, models, aliases); + if (!delegateMatch || skipWhitespaceForward(structure, delegateMatch.end) !== closeParenIndex) continue; + + const statementEnd = findStatementEnd(structure, openParenIndex); + const aliasEnd = simpleStaticAliasInitializerEnd(structure, closeParenIndex + 1, statementEnd); + if (aliasEnd === -1) continue; + + // 赋值式 method destructuring,例如 `let del; ({ delete: del } = db.auditLog)`。 + for (const name of names) { + methodAliases.push({ name, index: assignmentMatch.index, end: aliasEnd }); + } + } + + return methodAliases; +} + +function findStaticPrismaModelMethodBoundAliases(content, structure, models, methods, aliases, methodWrappers = [], methodAliases = []) { + const boundAliases = []; + const declarationPattern = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\b(?:\s*:[^=;]+)?\s*=/g; + const assignmentPattern = /\b([A-Za-z_$][\w$]*)\b\s*=/g; + + for (const declarationMatch of structure.matchAll(declarationPattern)) { + if (typeof declarationMatch.index !== "number") continue; + + const equalsIndex = structure.indexOf("=", declarationMatch.index); + if (equalsIndex === -1) continue; + + const initializerStart = skipWhitespaceForward(structure, equalsIndex + 1); + const statementEnd = findStatementEnd(structure, initializerStart); + const bindAliasEnd = staticPrismaModelMethodBoundAliasInitializerEnd( + content, + structure, + initializerStart, + statementEnd, + models, + methods, + aliases, + methodWrappers, + methodAliases + ); + if (bindAliasEnd === -1) continue; + + const aliasEnd = simpleStaticAliasInitializerEnd(structure, bindAliasEnd, statementEnd); + if (aliasEnd === -1) continue; + + // Prisma method `.bind(delegate)` alias 后续等价于直接 method invocation。 + boundAliases.push({ name: declarationMatch[1], index: declarationMatch.index, end: aliasEnd }); + } + + for (const assignmentMatch of structure.matchAll(assignmentPattern)) { + if (typeof assignmentMatch.index !== "number") continue; + + const variableName = assignmentMatch[1]; + const variableIndex = assignmentMatch.index + assignmentMatch[0].indexOf(variableName); + if (isMemberIdentifierReference(structure, variableIndex)) continue; + if (/\b(?:const|let|var)\s*$/.test(structure.slice(Math.max(0, assignmentMatch.index - 16), variableIndex))) continue; + + const equalsIndex = structure.indexOf("=", assignmentMatch.index + assignmentMatch[0].lastIndexOf("=")); + if (equalsIndex === -1) continue; + if ("=>"].includes(structure[equalsIndex + 1] ?? "")) continue; + + const initializerStart = skipWhitespaceForward(structure, equalsIndex + 1); + const statementEnd = findStatementEnd(structure, initializerStart); + const bindAliasEnd = staticPrismaModelMethodBoundAliasInitializerEnd( + content, + structure, + initializerStart, + statementEnd, + models, + methods, + aliases, + methodWrappers, + methodAliases + ); + if (bindAliasEnd === -1) continue; + + const aliasEnd = simpleStaticAliasInitializerEnd(structure, bindAliasEnd, statementEnd); + if (aliasEnd === -1) continue; + + // 后续赋值 alias 覆盖 `let update; update = db.model.method.bind(db.model)`,保持和声明式 bound alias 同一语义。 + boundAliases.push({ name: variableName, index: assignmentMatch.index, end: aliasEnd }); + } + + return boundAliases; +} + +function staticPrismaModelMethodBoundAliasInitializerEnd( + content, + structure, + sourceStart, + sourceEnd, + models, + methods, + aliases, + methodWrappers, + methodAliases +) { + const completeMatch = completeStaticPrismaModelMethodBoundAliasExpressionMatch( + content, + structure, + sourceStart, + sourceEnd, + models, + methods, + aliases, + methodWrappers, + methodAliases + ); + if (completeMatch) return completeMatch.end; + + const branchMatches = staticExpressionBranchSources(structure, sourceStart, sourceEnd) + .map((branch) => + completeStaticPrismaModelMethodBoundAliasExpressionMatch( + content, + structure, + branch.index, + branch.end, + models, + methods, + aliases, + methodWrappers, + methodAliases + ) + ) + .filter(Boolean); + + // 条件/逻辑表达式中任一静态分支是 guarded/audit/job method bind,后续 callable alias 按违规路径扫描。 + return branchMatches.length > 0 ? sourceEnd : -1; +} + +function completeStaticPrismaModelMethodBoundAliasExpressionMatch( + content, + structure, + sourceStart, + sourceEnd, + models, + methods, + aliases, + methodWrappers, + methodAliases +) { + const unwrapped = unwrapCompleteParenthesizedJavaScriptExpression(structure, sourceStart, sourceEnd); + const expressionStart = skipWhitespaceForward(structure, unwrapped.start); + const expressionEnd = skipWhitespaceBackward(structure, unwrapped.end - 1) + 1; + if (expressionStart >= expressionEnd) return null; + + const methodAccess = staticPrismaModelMethodAccessMatchAt( + content, + structure, + expressionStart, + models, + methods, + aliases, + methodWrappers, + methodAliases + ); + if (!methodAccess || methodAccess.end > expressionEnd) return null; + + const bindCloseParenIndex = prismaMethodBindCloseParenIndex(content, structure, methodAccess.end, expressionEnd); + if (bindCloseParenIndex === -1) return null; + // bind helper 返回的是右括号下标;这里用 exclusive end 严格校验,避免越过 ternary 分支边界的空白继续扫到 `:`。 + const bindExpressionEnd = bindCloseParenIndex + 1; + if (bindExpressionEnd !== expressionEnd) return null; + + return { index: expressionStart, end: expressionEnd }; +} + +function completeStaticPrismaModelMethodPlainAliasExpressionMatch( + content, + structure, + sourceStart, + sourceEnd, + models, + methods, + aliases, + methodWrappers, + methodAliases +) { + const unwrapped = unwrapCompleteParenthesizedJavaScriptExpression(structure, sourceStart, sourceEnd); + const expressionStart = skipWhitespaceForward(structure, unwrapped.start); + const expressionEnd = skipWhitespaceBackward(structure, unwrapped.end - 1) + 1; + if (expressionStart >= expressionEnd) return null; + + const methodAccess = staticPrismaModelMethodAccessMatchAt( + content, + structure, + expressionStart, + models, + methods, + aliases, + methodWrappers, + methodAliases + ); + if (!methodAccess || methodAccess.end > expressionEnd) return null; + if (!isAtStaticWrapperExpressionEnd(structure, methodAccess.end, expressionEnd)) return null; + + return { index: expressionStart, end: expressionEnd }; +} + +function staticPrismaModelMethodPlainAliasInitializerEnd( + content, + structure, + sourceStart, + sourceEnd, + models, + methods, + aliases, + methodWrappers, + methodAliases +) { + const plainMatch = staticInitializerBranchMatch( + content, + structure, + sourceStart, + sourceEnd, + (scanContent, scanStructure, branchStart, branchEnd) => + completeStaticPrismaModelMethodPlainAliasExpressionMatch( + scanContent, + scanStructure, + branchStart, + branchEnd, + models, + methods, + aliases, + methodWrappers, + methodAliases + ) + ); + + // 条件/逻辑表达式中任一静态分支是 Prisma plain method,后续 callable alias 仍按 Prisma 写入扫描。 + return plainMatch ? (plainMatch.branch ? sourceEnd : plainMatch.end) : -1; +} + +function findStaticPrismaModelMethodPlainAliases(content, structure, models, methods, aliases, methodWrappers = [], methodAliases = []) { + const plainAliases = []; + const declarationPattern = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\b(?:\s*:[^=;]+)?\s*=/g; + const assignmentPattern = /\b([A-Za-z_$][\w$]*)\b\s*=/g; + + for (const declarationMatch of structure.matchAll(declarationPattern)) { + if (typeof declarationMatch.index !== "number") continue; + + const equalsIndex = structure.indexOf("=", declarationMatch.index); + if (equalsIndex === -1) continue; + + const initializerStart = skipWhitespaceForward(structure, equalsIndex + 1); + const statementEnd = findStatementEnd(structure, initializerStart); + const plainAliasEnd = staticPrismaModelMethodPlainAliasInitializerEnd( + content, + structure, + initializerStart, + statementEnd, + models, + methods, + aliases, + methodWrappers, + methodAliases + ); + if (plainAliasEnd === -1) continue; + + const aliasEnd = simpleStaticAliasInitializerEnd(structure, plainAliasEnd, statementEnd); + if (aliasEnd === -1) continue; + + // `const del = db.job.delete` 这类 plain method alias 后续通过 direct/call/apply/bind 都等价于 Prisma method invocation。 + plainAliases.push({ name: declarationMatch[1], index: declarationMatch.index, end: aliasEnd }); + } + + for (const assignmentMatch of structure.matchAll(assignmentPattern)) { + if (typeof assignmentMatch.index !== "number") continue; + + const variableName = assignmentMatch[1]; + const variableIndex = assignmentMatch.index + assignmentMatch[0].indexOf(variableName); + if (isMemberIdentifierReference(structure, variableIndex)) continue; + if (/\b(?:const|let|var)\s*$/.test(structure.slice(Math.max(0, assignmentMatch.index - 16), variableIndex))) continue; + + const equalsIndex = structure.indexOf("=", assignmentMatch.index + assignmentMatch[0].lastIndexOf("=")); + if (equalsIndex === -1) continue; + if ("=>"].includes(structure[equalsIndex + 1] ?? "")) continue; + + const initializerStart = skipWhitespaceForward(structure, equalsIndex + 1); + const statementEnd = findStatementEnd(structure, initializerStart); + const plainAliasEnd = staticPrismaModelMethodPlainAliasInitializerEnd( + content, + structure, + initializerStart, + statementEnd, + models, + methods, + aliases, + methodWrappers, + methodAliases + ); + if (plainAliasEnd === -1) continue; + + const aliasEnd = simpleStaticAliasInitializerEnd(structure, plainAliasEnd, statementEnd); + if (aliasEnd === -1) continue; + + // 后续赋值 alias 覆盖 `let update; update = db.gameVersion.update`,保持和声明式 plain alias 同一语义。 + plainAliases.push({ name: variableName, index: assignmentMatch.index, end: aliasEnd }); + } + + return plainAliases; +} + +function staticExpressionBranchSources(structure, sourceStart, sourceEnd) { + const unwrapped = unwrapCompleteParenthesizedJavaScriptExpression(structure, sourceStart, sourceEnd); + const expressionStart = skipWhitespaceForward(structure, unwrapped.start); + const expressionEnd = skipWhitespaceBackward(structure, unwrapped.end - 1) + 1; + if (expressionStart >= expressionEnd) return []; + + const ternary = topLevelTernaryExpressionParts(structure, expressionStart, expressionEnd); + if (ternary) return [ternary.whenTrue, ternary.whenFalse]; + + const logical = topLevelLogicalExpressionParts(structure, expressionStart, expressionEnd); + return logical ?? []; +} + +function staticInitializerBranchMatch(content, structure, sourceStart, sourceEnd, matcher) { + const completeMatch = matcher(content, structure, sourceStart, sourceEnd); + if (completeMatch) return completeMatch; + + for (const branch of staticExpressionBranchSources(structure, sourceStart, sourceEnd)) { + // 分支表达式递归展开,确保 `a ? safe : (b || db.model.method)` 这类嵌套静态分支不会 fail-open。 + const branchMatch = staticInitializerBranchMatch(content, structure, branch.index, branch.end, matcher); + if (branchMatch) return { ...branchMatch, branch }; + } + + return null; +} + +function topLevelTernaryExpressionParts(structure, sourceStart, sourceEnd) { + let parenDepth = 0; + let braceDepth = 0; + let bracketDepth = 0; + let questionIndex = -1; + + for (let cursor = sourceStart; cursor < sourceEnd; cursor += 1) { + const char = structure[cursor]; + if (char === "(") parenDepth += 1; + if (char === ")") parenDepth -= 1; + if (char === "{") braceDepth += 1; + if (char === "}") braceDepth -= 1; + if (char === "[") bracketDepth += 1; + if (char === "]") bracketDepth -= 1; + if (parenDepth !== 0 || braceDepth !== 0 || bracketDepth !== 0) continue; + + if (char === "?" && structure[cursor + 1] !== ".") { + questionIndex = cursor; + break; + } + } + + if (questionIndex === -1) return null; + + parenDepth = 0; + braceDepth = 0; + bracketDepth = 0; + let nestedQuestionDepth = 0; + + for (let cursor = questionIndex + 1; cursor < sourceEnd; cursor += 1) { + const char = structure[cursor]; + if (char === "(") parenDepth += 1; + if (char === ")") parenDepth -= 1; + if (char === "{") braceDepth += 1; + if (char === "}") braceDepth -= 1; + if (char === "[") bracketDepth += 1; + if (char === "]") bracketDepth -= 1; + if (parenDepth !== 0 || braceDepth !== 0 || bracketDepth !== 0) continue; + + if (char === "?" && structure[cursor + 1] !== ".") { + nestedQuestionDepth += 1; + continue; + } + + if (char !== ":") continue; + if (nestedQuestionDepth > 0) { + nestedQuestionDepth -= 1; + continue; + } + + return { + whenTrue: { index: skipWhitespaceForward(structure, questionIndex + 1), end: skipWhitespaceBackward(structure, cursor - 1) + 1 }, + whenFalse: { index: skipWhitespaceForward(structure, cursor + 1), end: sourceEnd } + }; + } + + return null; +} + +function topLevelLogicalExpressionParts(structure, sourceStart, sourceEnd) { + let parenDepth = 0; + let braceDepth = 0; + let bracketDepth = 0; + + for (let cursor = sourceStart; cursor < sourceEnd - 1; cursor += 1) { + const char = structure[cursor]; + if (char === "(") parenDepth += 1; + if (char === ")") parenDepth -= 1; + if (char === "{") braceDepth += 1; + if (char === "}") braceDepth -= 1; + if (char === "[") bracketDepth += 1; + if (char === "]") bracketDepth -= 1; + if (parenDepth !== 0 || braceDepth !== 0 || bracketDepth !== 0) continue; + + const operator = structure.slice(cursor, cursor + 2); + if (operator !== "||" && operator !== "&&") continue; + + return [ + { index: sourceStart, end: skipWhitespaceBackward(structure, cursor - 1) + 1 }, + { index: skipWhitespaceForward(structure, cursor + 2), end: sourceEnd } + ]; + } + + return null; +} + +function prismaMethodBindCloseParenIndex(content, structure, methodAccessEnd, statementEnd) { + const operation = staticCallableMemberOperationMatchAt(content, structure, methodAccessEnd, ["bind"]); + if (!operation) return -1; + if (operation.closeParenIndex === -1 || operation.closeParenIndex > statementEnd) return -1; + return operation.closeParenIndex; +} + function prismaDelegateAliasMethodCallPattern(aliasNames, methods) { const aliases = aliasNames.map(escapeRegExp).join("|"); return new RegExp(String.raw`\b(${aliases})\b\s*${staticPrismaPropertyAccessPattern(methods)}\s*(?:\?\.)?\s*\(`, "g"); } -function prismaModelMethodCallMatchesInStructure(structure, models, methods) { - const matches = []; +function staticPrismaModelMethodAccessMatchAt(content, structure, index, models, methods, aliases = [], methodWrappers = [], methodAliases = []) { + const accessStart = skipWhitespaceForward(structure, index); - for (const match of structure.matchAll(prismaModelMethodCallPattern(models, methods))) { - if (typeof match.index === "number") matches.push({ index: match.index }); + const receiverMatch = /^(?:[A-Za-z_$][\w$]*|this)\b/.exec(structure.slice(accessStart)); + if (receiverMatch) { + const modelAccessStart = skipWhitespaceForward(structure, accessStart + receiverMatch[0].length); + const modelAccess = staticPrismaPropertyAccessMatchAt(content, structure, modelAccessStart, models, modelAccessStart); + if (modelAccess) { + const methodAccessStart = skipWhitespaceForward(structure, modelAccess.end); + const methodAccess = staticPrismaPropertyAccessMatchAt(content, structure, methodAccessStart, methods, methodAccessStart); + if (methodAccess) return { index: modelAccess.index, end: methodAccess.end }; + } } - const aliases = findStaticPrismaDelegateAliases(structure, models); - if (aliases.length === 0) return matches; + const wrapperUsage = staticWrapperSourceUsageMatchAt(content, structure, methodWrappers, accessStart, accessStart); + if (wrapperUsage) return { index: wrapperUsage.index, end: wrapperUsage.end }; - const aliasPattern = prismaDelegateAliasMethodCallPattern( - [...new Set(aliases.map((alias) => alias.name))], - methods + const methodAliasMatch = /^[A-Za-z_$][\w$]*/.exec(structure.slice(accessStart)); + if (methodAliasMatch && !isMemberIdentifierReference(structure, accessStart)) { + const aliasName = methodAliasMatch[0]; + if (latestStaticAliasRecordAt(structure, methodAliases, aliasName, accessStart)) { + return { index: accessStart, end: accessStart + aliasName.length }; + } + } + + const aliasMatch = /^[A-Za-z_$][\w$]*/.exec(structure.slice(accessStart)); + if (!aliasMatch || aliases.length === 0) return null; + + const aliasName = aliasMatch[0]; + if (!latestStaticAliasRecordAt(structure, aliases, aliasName, accessStart)) return null; + if (isMemberIdentifierReference(structure, accessStart)) return null; + + const methodAccessStart = skipWhitespaceForward(structure, accessStart + aliasName.length); + const methodAccess = staticPrismaPropertyAccessMatchAt(content, structure, methodAccessStart, methods, methodAccessStart); + return methodAccess ? { index: accessStart, end: methodAccess.end } : null; +} + +function staticPrismaModelMethodAccessArgumentMatch(content, structure, argument, models, methods, aliases, methodWrappers = [], methodAliases = []) { + if (!argument) return null; + + const targetStart = skipWhitespaceForward(structure, argument.index); + const targetEnd = skipWhitespaceBackward(structure, argument.end - 1) + 1; + if (targetStart >= targetEnd) return null; + + const accessMatch = staticPrismaModelMethodAccessMatchAt( + content, + structure, + targetStart, + models, + methods, + aliases, + methodWrappers, + methodAliases + ); + if (!accessMatch) return null; + + return skipWhitespaceForward(structure, accessMatch.end) === targetEnd ? accessMatch : null; +} + +function prismaMethodEquivalentInvocationMatch(content, structure, methodAccessEnd) { + const callStart = hasOptionalCallOperatorAt(structure, methodAccessEnd); + if (structure[callStart] === "(") return { callStart }; + + const operation = staticCallableMemberOperationMatchAt(content, structure, methodAccessEnd, ["call", "apply", "bind"]); + if (!operation) return null; + + if (operation.kind === "call" || operation.kind === "apply") { + return { callStart: operation.openParenIndex }; + } + + let boundCallStart = skipWhitespaceForward(structure, operation.closeParenIndex + 1); + if (structure.startsWith("?.", boundCallStart)) { + boundCallStart = skipWhitespaceForward(structure, boundCallStart + 2); + } + if (structure[boundCallStart] !== "(") return null; + + const boundCallEnd = findMatchingParen(structure, boundCallStart); + return boundCallEnd === -1 ? null : { callStart: boundCallStart }; +} + +function prismaFunctionPrototypeMethodCallMatchesInStructure(content, structure, models, methods, aliases, builtinInvokerAliases, methodWrappers, methodAliases) { + const matches = []; + const boundTargetAliases = findFunctionPrototypeBoundTargetAliases(content, structure); + + for (const invocation of functionPrototypeCallInvocationMatches(content, structure, builtinInvokerAliases)) { + const targetArgument = functionPrototypeEquivalentArgumentSource(content, structure, invocation, 0); + const targetAccess = staticPrismaModelMethodAccessArgumentMatch(content, structure, targetArgument, models, methods, aliases, methodWrappers, methodAliases); + if (!targetAccess) continue; + + // Function.prototype.call/apply 的目标函数在参数位;命中后复用外层调用体做 Prisma 写入扫描。 + matches.push({ index: targetAccess.index, callStart: invocation.openParenIndex }); + } + + for (const invocation of reflectApplyInvocationMatches(content, structure, builtinInvokerAliases)) { + const reflectTargetArgument = reflectApplyEquivalentArgumentSource(content, structure, invocation, 0); + if (!functionPrototypeMethodKindFromArgument(content, structure, reflectTargetArgument, builtinInvokerAliases)) { + const reflectTargetAccess = staticPrismaModelMethodAccessArgumentMatch( + content, + structure, + reflectTargetArgument, + models, + methods, + aliases, + methodWrappers, + methodAliases + ); + if (reflectTargetAccess) { + // Reflect.apply(method, receiver, [args]) 等价执行 Prisma method target。 + matches.push({ index: reflectTargetAccess.index, callStart: invocation.openParenIndex }); + } + continue; + } + + const targetArgument = reflectApplyEquivalentArgumentSource(content, structure, invocation, 1); + const targetAccess = staticPrismaModelMethodAccessArgumentMatch(content, structure, targetArgument, models, methods, aliases, methodWrappers, methodAliases); + if (!targetAccess) continue; + + matches.push({ index: targetAccess.index, callStart: invocation.openParenIndex }); + } + + for (const invocation of functionPrototypeBoundTargetInvocationMatches(content, structure, boundTargetAliases)) { + const targetAccess = staticPrismaModelMethodAccessArgumentMatch( + content, + structure, + invocation.targetArgument, + models, + methods, + aliases, + methodWrappers, + methodAliases + ); + if (!targetAccess) continue; + + // Function.prototype.call/apply.bind(method)(...) 绑定的是真实 Prisma method target。 + matches.push({ index: targetAccess.index, callStart: invocation.openParenIndex }); + } + + return matches; +} + +function prismaModelMethodCallMatchesInStructure(content, structure, models, methods) { + const scanStructure = stripParenthesesAroundStaticMemberChains(content, structure); + const matches = []; + + for (const modelAccess of staticPrismaPropertyAccessMatches(content, scanStructure, models)) { + const methodAccessStart = skipWhitespaceForward(scanStructure, modelAccess.end); + const methodAccess = staticPrismaPropertyAccessMatchAt(content, scanStructure, methodAccessStart, methods, methodAccessStart); + if (!methodAccess) continue; + + const invocation = prismaMethodEquivalentInvocationMatch(content, scanStructure, methodAccess.end); + if (invocation) matches.push({ index: modelAccess.index, callStart: invocation.callStart }); + } + + const aliases = findStaticPrismaDelegateAliases(content, scanStructure, models); + const builtinInvokerAliases = findStaticBuiltinInvokerAliases(content, scanStructure); + const delegateWrappers = prismaDelegateWrapperSources(content, scanStructure, models, aliases); + const initialMethodWrappers = prismaModelMethodWrapperSources(content, scanStructure, models, methods, aliases); + const initialMethodAliases = prismaModelMethodWrapperAliases(content, scanStructure, initialMethodWrappers) + .concat(findStaticPrismaDestructuredModelMethodAliases(content, scanStructure, models, methods, aliases)) + .concat(findStaticPrismaAssignedDestructuredModelMethodAliases(content, scanStructure, models, methods, aliases)) + .concat(findStaticPrismaModelMethodPlainAliases(content, scanStructure, models, methods, aliases, initialMethodWrappers)); + const boundMethodAliases = findStaticPrismaModelMethodBoundAliases( + content, + scanStructure, + models, + methods, + aliases, + initialMethodWrappers, + initialMethodAliases + ); + const methodAliases = propagateStaticIdentifierAliases( + scanStructure, + initialMethodAliases.concat(boundMethodAliases), + (_sourceAlias, initializer) => ({ name: initializer.name, index: initializer.index, end: initializer.end }) + ); + const methodWrappers = prismaModelMethodWrapperSources(content, scanStructure, models, methods, aliases, methodAliases); + methodAliases.push(...prismaModelMethodWrapperAliases(content, scanStructure, methodWrappers)); + + matches.push( + ...prismaFunctionPrototypeMethodCallMatchesInStructure( + content, + scanStructure, + models, + methods, + aliases, + builtinInvokerAliases, + methodWrappers, + methodAliases + ) ); - for (const match of structure.matchAll(aliasPattern)) { - if (typeof match.index !== "number") continue; - const aliasName = match[1]; - if (aliases.some((alias) => alias.name === aliasName && alias.index < match.index)) { - matches.push({ index: match.index }); + for (const delegateWrapperUsage of staticWrapperSourceUsageMatches(content, scanStructure, delegateWrappers)) { + const methodAccessStart = skipWhitespaceForward(scanStructure, delegateWrapperUsage.end); + const methodAccess = staticPrismaPropertyAccessMatchAt(content, scanStructure, methodAccessStart, methods, methodAccessStart); + if (!methodAccess) continue; + + const invocation = prismaMethodEquivalentInvocationMatch(content, scanStructure, methodAccess.end); + if (invocation) matches.push({ index: delegateWrapperUsage.index, callStart: invocation.callStart }); + } + + for (const methodWrapperUsage of staticWrapperSourceUsageMatches(content, scanStructure, methodWrappers)) { + const invocation = prismaMethodEquivalentInvocationMatch(content, scanStructure, methodWrapperUsage.end); + if (invocation) matches.push({ index: methodWrapperUsage.index, callStart: invocation.callStart }); + } + + const methodAliasNames = [...new Set(methodAliases.map((alias) => alias.name))]; + const methodAliasPattern = + methodAliasNames.length > 0 ? new RegExp(String.raw`\b(${methodAliasNames.map(escapeRegExp).join("|")})\b`, "g") : null; + + if (methodAliasPattern) { + for (const match of scanStructure.matchAll(methodAliasPattern)) { + if (typeof match.index !== "number") continue; + const aliasName = match[1]; + if (!latestStaticAliasRecordAt(scanStructure, methodAliases, aliasName, match.index)) continue; + if (isMemberIdentifierReference(scanStructure, match.index)) continue; + + const invocation = prismaMethodEquivalentInvocationMatch(content, scanStructure, match.index + aliasName.length); + if (invocation) matches.push({ index: match.index, callStart: invocation.callStart }); + } + } + + const aliasNames = [...new Set(aliases.map((alias) => alias.name))]; + const aliasPattern = aliasNames.length > 0 ? new RegExp(String.raw`\b(${aliasNames.map(escapeRegExp).join("|")})\b`, "g") : null; + + if (aliasPattern) { + for (const match of scanStructure.matchAll(aliasPattern)) { + if (typeof match.index !== "number") continue; + const aliasName = match[1]; + if (!latestStaticAliasRecordAt(scanStructure, aliases, aliasName, match.index)) continue; + if (isMemberIdentifierReference(scanStructure, match.index)) continue; + + const methodAccessStart = skipWhitespaceForward(scanStructure, match.index + aliasName.length); + const methodAccess = staticPrismaPropertyAccessMatchAt(content, scanStructure, methodAccessStart, methods, methodAccessStart); + if (!methodAccess) continue; + + const invocation = prismaMethodEquivalentInvocationMatch(content, scanStructure, methodAccess.end); + if (invocation) matches.push({ index: match.index, callStart: invocation.callStart }); } } @@ -395,7 +3382,7 @@ function prismaModelMethodCallMatchesInStructure(structure, models, methods) { function prismaModelMethodCallMatches(content, models, methods) { const structure = stripStringLiteralsExceptPropertyKeys(maskCommentsOutsideStringLiterals(content)); - return prismaModelMethodCallMatchesInStructure(structure, models, methods); + return prismaModelMethodCallMatchesInStructure(content, structure, models, methods); } function sqlTableListContainingPattern(targetTableReference) { @@ -434,6 +3421,9 @@ function skipWhitespaceBackward(content, index) { } function isStaticQuotedPropertyKeyOrAccess(content, literalStart, literalEnd) { + const staticComputedSpan = staticComputedStringPropertyAccessSpan(content, literalStart); + if (staticComputedSpan) return true; + const afterLiteral = skipWhitespaceForward(content, literalEnd); if (content[afterLiteral] === ":") return true; @@ -449,10 +3439,57 @@ function isStaticQuotedPropertyKeyOrAccess(content, literalStart, literalEnd) { content[afterComputedKey] === "." || content[afterComputedKey] === "?" || content[afterComputedKey] === "[" || - content[afterComputedKey] === "(" + content[afterComputedKey] === "(" || + content[afterComputedKey] === ";" || + content[afterComputedKey] === "," || + content[afterComputedKey] === ")" || + content[afterComputedKey] === "}" || + typeof content[afterComputedKey] === "undefined" ); } +function staticComputedStringPropertyAccessSpan(content, literalStart) { + const openBracketIndex = content.lastIndexOf("[", literalStart); + if (openBracketIndex === -1) return null; + if (content.lastIndexOf("]", literalStart) > openBracketIndex) return null; + + const structure = maskStringLiterals(maskCommentsOutsideStringLiterals(content)); + const closeBracketIndex = findMatchingBracket(structure, openBracketIndex); + if (closeBracketIndex === -1 || literalStart > closeBracketIndex) return null; + + const afterComputedKey = skipWhitespaceForward(content, closeBracketIndex + 1); + if ( + !( + content[afterComputedKey] === ":" || + content[afterComputedKey] === "." || + content[afterComputedKey] === "?" || + content[afterComputedKey] === "[" || + content[afterComputedKey] === "(" || + content[afterComputedKey] === ";" || + content[afterComputedKey] === "," || + content[afterComputedKey] === ")" || + content[afterComputedKey] === "}" || + typeof content[afterComputedKey] === "undefined" + ) + ) { + return null; + } + + const expressionStart = openBracketIndex + 1; + const expressionEnd = closeBracketIndex; + const values = staticJavaScriptExpressionValues( + content, + structure, + content.slice(expressionStart, expressionEnd), + expressionStart, + openBracketIndex, + new Set(), + 0 + ); + + return values.length === 1 ? { start: openBracketIndex, end: closeBracketIndex + 1 } : null; +} + function stripStringLiteralsExceptPropertyKeys(content) { let output = ""; @@ -618,6 +3655,49 @@ function findMatchingBrace(content, openBraceIndex) { return -1; } +function findMatchingBracket(content, openBracketIndex) { + let depth = 0; + for (let index = openBracketIndex; index < content.length; index += 1) { + const char = content[index]; + if (char === "[") depth += 1; + if (char === "]") { + depth -= 1; + if (depth === 0) return index; + } + } + return -1; +} + +function stripParenthesesAroundStaticMemberChains(content, structure) { + let output = structure; + let changed = false; + + do { + changed = false; + for (let index = 0; index < output.length; index += 1) { + if (output[index] !== "(") continue; + if (!isParenthesizedExpressionOpen(output, index)) continue; + + const closeParenIndex = findMatchingParen(output, index); + if (closeParenIndex === -1) continue; + + const innerStart = skipWhitespaceForward(output, index + 1); + const innerEnd = skipWhitespaceBackward(output, closeParenIndex - 1) + 1; + if (innerStart >= innerEnd) continue; + + const memberMatch = simpleStaticMemberChainMatchAt(content, output, innerStart); + if (!memberMatch || innerStart + memberMatch[0].length !== innerEnd) continue; + + // Prisma/raw callee 允许完整 member chain 外包多层括号;剥离括号后仍保留源码索引长度。 + output = `${output.slice(0, index)} ${output.slice(index + 1, closeParenIndex)} ${output.slice(closeParenIndex + 1)}`; + changed = true; + break; + } + } while (changed); + + return output; +} + function maskStringLiterals(content) { let output = ""; let index = 0; @@ -695,16 +3775,22 @@ function hasPrismaCallContaining(content, callPattern, termPattern) { return false; } +function hasPrismaCallBodyContaining(scanContent, callStart, termPattern) { + const openParenIndex = scanContent[callStart] === "(" ? callStart : scanContent.indexOf("(", callStart); + if (openParenIndex === -1) return false; + + const closeParenIndex = findMatchingParen(scanContent, openParenIndex); + if (closeParenIndex === -1) return false; + + const callBody = scanContent.slice(openParenIndex, closeParenIndex + 1); + return termPattern.test(callBody); +} + function hasPrismaModelMethodCallContaining(content, models, methods, termPattern) { // delegate alias 与直接 model access 共用同一调用体解析,避免 Job/Audit/guarded 边界分叉。 const scanContent = stripStringLiteralsExceptPropertyKeys(maskCommentsOutsideStringLiterals(content)); - for (const match of prismaModelMethodCallMatchesInStructure(scanContent, models, methods)) { - const openParenIndex = scanContent.indexOf("(", match.index); - if (openParenIndex === -1) continue; - const closeParenIndex = findMatchingParen(scanContent, openParenIndex); - if (closeParenIndex === -1) continue; - const callBody = scanContent.slice(openParenIndex, closeParenIndex + 1); - if (termPattern.test(callBody)) return true; + for (const match of prismaModelMethodCallMatchesInStructure(content, scanContent, models, methods)) { + if (hasPrismaCallBodyContaining(scanContent, match.callStart, termPattern)) return true; } return false; } @@ -742,15 +3828,19 @@ function hasRawJobRuntimeFieldWrite(sqlSource) { const identifier = sqlBareIdentifierPattern(); const runtimeField = String.raw`(?:${jobRuntimeFields.map(escapeRegExp).join("|")})`; const maybeQualifiedRuntimeField = String.raw`(?:${identifier}\s*\.\s*)?${runtimeField}`; + // PostgreSQL UPDATE 允许目标表后带 `*` 继承标记;扫描时必须先吃掉它再匹配 alias/SET。 const updateJobRuntimeFieldPattern = new RegExp( - String.raw`\bUPDATE\s+(?:ONLY\s+)?${jobTable}(?:\s+(?:AS\s+)?${identifier})?\s+SET\b[\s\S]*?\b${maybeQualifiedRuntimeField}\s*=`, + String.raw`\bUPDATE\s+(?:ONLY\s+)?${jobTable}(?:\s*\*)?(?:\s+(?:AS\s+)?${identifier})?\s+SET\b[\s\S]*?\b${maybeQualifiedRuntimeField}\s*=`, "i" ); const updateJobTablePattern = new RegExp( - String.raw`\bUPDATE\s+(?:ONLY\s+)?${jobTable}(?:\s+(?:AS\s+)?${identifier})?\s+SET\b`, + String.raw`\bUPDATE\s+(?:ONLY\s+)?${jobTable}(?:\s*\*)?(?:\s+(?:AS\s+)?${identifier})?\s+SET\b`, + "i" + ); + const mergeJobTablePattern = new RegExp( + String.raw`\bMERGE\s+INTO\s+(?:ONLY\s+)?${jobTable}(?:\s+(?:AS\s+)?${identifier})?\b`, "i" ); - const mergeJobTablePattern = new RegExp(String.raw`\bMERGE\s+INTO\s+${jobTable}(?:\s+(?:AS\s+)?${identifier})?\b`, "i"); return ( updateJobRuntimeFieldPattern.test(sql) || @@ -1043,16 +4133,21 @@ function hasDynamicRawSqlWrite(sqlSource) { const triggerIdentifier = sqlTriggerIdentifierPattern(); const dynamicTableReference = String.raw`(?:${dynamicIdentifier}|${identifier}\s*\.\s*${dynamicIdentifier}|${dynamicIdentifier}\s*\.\s*${identifier})`; const dynamicFieldReference = String.raw`(?:${dynamicIdentifier}|${identifier}\s*\.\s*${dynamicIdentifier}|${dynamicIdentifier}\s*\.\s*${identifier})`; + const dynamicFunctionReference = String.raw`(?:${dynamicIdentifier}|${identifier}\s*\.\s*${dynamicIdentifier}|${dynamicIdentifier}\s*\.\s*${identifier})(?:\s*\([^)]*\))?`; + const dynamicFunctionList = sqlFunctionListContainingPattern(dynamicFunctionReference); const anyTableReference = sqlAnyTableReferencePattern(); // raw 写 SQL 里的动态表/字段/trigger identifier 无法静态证明边界,必须 fail-closed。 return ( - new RegExp(String.raw`\bUPDATE\s+(?:ONLY\s+)?${dynamicTableReference}(?:\s+(?:AS\s+)?${identifier})?\s+SET\b`, "i").test( + new RegExp(String.raw`\bUPDATE\s+(?:ONLY\s+)?${dynamicTableReference}(?:\s*\*)?(?:\s+(?:AS\s+)?${identifier})?\s+SET\b`, "i").test( sql ) || new RegExp(String.raw`\bUPDATE\b[\s\S]*?\bSET\b[\s\S]*?\b${dynamicFieldReference}\s*=`, "i").test(sql) || new RegExp(String.raw`\bINSERT\s+INTO\s+${dynamicTableReference}\b`, "i").test(sql) || new RegExp(String.raw`\bINSERT\s+INTO\b[\s\S]*?\([^)]*\b${dynamicFieldReference}\b[^)]*\)\s+VALUES\b`, "i").test(sql) || + new RegExp(String.raw`\bMERGE\s+INTO\s+(?:ONLY\s+)?${dynamicTableReference}(?:\s+(?:AS\s+)?${identifier})?\b`, "i").test( + sql + ) || new RegExp(String.raw`\bDELETE\s+FROM\s+(?:ONLY\s+)?${dynamicTableReference}\b`, "i").test(sql) || new RegExp(sqlTruncateTableListPattern(dynamicTableReference), "i").test(sql) || new RegExp(sqlDropTableListPattern(dynamicTableReference), "i").test(sql) || @@ -1064,7 +4159,11 @@ function hasDynamicRawSqlWrite(sqlSource) { "i" ).test(sql) || new RegExp(String.raw`\bALTER\s+TABLE\s+(?:IF\s+EXISTS\s+)?(?:ONLY\s+)?${dynamicTableReference}\b`, "i").test(sql) || - new RegExp(String.raw`\bALTER\s+TABLE\b[\s\S]*?\bDISABLE\s+TRIGGER\s+${dynamicFieldReference}\b`, "i").test(sql) + new RegExp(String.raw`\bALTER\s+TABLE\b[\s\S]*?\bDISABLE\s+TRIGGER\s+${dynamicFieldReference}\b`, "i").test(sql) || + // 函数 DDL 可以替换/移除 AuditLog append-only guard,动态函数名必须 fail-closed。 + new RegExp(String.raw`\bDROP\s+(?:FUNCTION|ROUTINE)\s+(?:IF\s+EXISTS\s+)?${dynamicFunctionList}(?=\s|;|$)`, "i").test(sql) || + new RegExp(String.raw`\bALTER\s+(?:FUNCTION|ROUTINE)\s+(?:IF\s+EXISTS\s+)?${dynamicFunctionReference}(?=\s|;|$)`, "i").test(sql) || + new RegExp(String.raw`\bCREATE\s+(?:OR\s+REPLACE\s+)?FUNCTION\s+${dynamicFunctionReference}(?=\s|,|;|$)`, "i").test(sql) ); } @@ -1135,8 +4234,114 @@ function rawSqlStructure(content) { return stripStringLiteralsExceptPropertyKeys(maskCommentsOutsideStringLiterals(content)); } -function rawSqlMemberOperation(structure, callStart) { +function findOpeningParen(content, closeParenIndex) { + let depth = 0; + for (let index = closeParenIndex; index >= 0; index -= 1) { + const char = content[index]; + if (char === ")") depth += 1; + if (char === "(") { + depth -= 1; + if (depth === 0) return index; + } + } + return -1; +} + +function isParenthesizedExpressionOpen(structure, openParenIndex) { + const previousIndex = skipWhitespaceBackward(structure, openParenIndex - 1); + if (previousIndex < 0) return true; + + const previous = structure[previousIndex]; + if ("([{=,:;!?&|+-*%^~<>".includes(previous)) return true; + + if (!/[\w$]/.test(previous)) return false; + + const previousWord = /[A-Za-z_$][\w$]*$/.exec(structure.slice(0, previousIndex + 1))?.[0] ?? ""; + return /^(?:return|throw|await|yield|case|delete|typeof|void|new|in|of)$/.test(previousWord); +} + +function skipOptionalTypeArguments(structure, index) { + let cursor = skipWhitespaceForward(structure, index); + if (structure.startsWith("?.", cursor)) { + const optionalCursor = skipWhitespaceForward(structure, cursor + 2); + if (structure[optionalCursor] !== "<") return cursor; + + const optionalTypeEnd = staticTypeArgumentsEnd(structure, optionalCursor); + if (optionalTypeEnd === -1) return cursor; + + const afterOptionalType = skipWhitespaceForward(structure, optionalTypeEnd); + return structure[afterOptionalType] === "(" ? afterOptionalType : cursor; + } + + if (structure[cursor] !== "<") return cursor; + + const typeEnd = staticTypeArgumentsEnd(structure, cursor); + if (typeEnd === -1) return cursor; + + const afterType = skipWhitespaceForward(structure, typeEnd); + return structure[afterType] === "(" ? afterType : cursor; +} + +function staticTypeArgumentsEnd(structure, openAngleIndex) { + if (structure[openAngleIndex] !== "<") return -1; + + let depth = 1; + let sawTypeToken = false; + + for (let cursor = openAngleIndex + 1; cursor < structure.length; cursor += 1) { + const char = structure[cursor]; + + if (char === '"' || char === "'" || char === "`") { + const literalEnd = findStringLiteralEnd(structure, cursor); + if (literalEnd === -1) return -1; + cursor = literalEnd; + sawTypeToken = true; + continue; + } + + if (char === "<") { + depth += 1; + sawTypeToken = true; + continue; + } + + if (char === ">") { + depth -= 1; + if (depth === 0) return sawTypeToken ? cursor + 1 : -1; + continue; + } + + if (char === "(" || char === ")" || char === "{" || char === "}" || char === ";" || char === "\n" || char === "\r") { + return -1; + } + + if (!/\s/.test(char)) sawTypeToken = true; + } + + return -1; +} + +function parenthesizedRawSqlOperationStart(structure, rawAccessEnd, closeParenIndex) { + let cursor = closeParenIndex; + + while (structure[cursor] === ")") { + const openParenIndex = findOpeningParen(structure, cursor); + if (openParenIndex === -1 || !isParenthesizedExpressionOpen(structure, openParenIndex)) return -1; + if (rawAccessEnd > cursor) return -1; + + cursor = skipWhitespaceForward(structure, cursor + 1); + } + + return cursor; +} + +function rawSqlMemberOperation(content, structure, callStart) { const operationStart = skipWhitespaceForward(structure, callStart); + const genericCallStart = skipOptionalTypeArguments(structure, operationStart); + if (genericCallStart !== operationStart && structure[genericCallStart] === "(") { + // raw executor 的 TS 泛型调用 `raw(...)` / `raw?.(...)` 与普通调用等价,SQL 参数仍从调用括号读取。 + return { kind: "direct", callStart: genericCallStart }; + } if (structure[operationStart] === "(") { return { kind: "direct", callStart: operationStart }; @@ -1146,39 +4351,35 @@ function rawSqlMemberOperation(structure, callStart) { return { kind: "template", templateStart: operationStart }; } + if (structure[operationStart] === ")") { + // raw executor 被括号包成 callee 时,真实调用操作在 closing paren 之后。 + const parenthesizedOperationStart = parenthesizedRawSqlOperationStart(structure, callStart, operationStart); + if (parenthesizedOperationStart !== -1) { + return rawSqlMemberOperation(content, structure, parenthesizedOperationStart); + } + } + if (structure.startsWith("?.", operationStart)) { const optionalTargetStart = skipWhitespaceForward(structure, operationStart + 2); if (structure[optionalTargetStart] === "(") { return { kind: "direct", callStart: optionalTargetStart }; } - // `raw?.call(...)` / `raw?.bind(...)` 的 method name 在 `?.` 后面,不能先吃掉 `?.` 再要求 method 前缀。 - const optionalMethodMatch = /^(call|bind)\b/.exec(structure.slice(optionalTargetStart)); - if (optionalMethodMatch) { - return { - kind: optionalMethodMatch[1], - methodNameEnd: optionalTargetStart + optionalMethodMatch[1].length - }; - } + const optionalOperation = staticCallableMemberOperationMatchAt(content, structure, operationStart, ["call", "bind", "apply"]); + if (optionalOperation) return optionalOperation; } - const methodMatch = /^(?:\.|\?\.)\s*(call|bind)\b/.exec(structure.slice(operationStart)); - if (!methodMatch) return null; - - return { - kind: methodMatch[1], - methodNameEnd: operationStart + methodMatch[0].length - }; + return staticCallableMemberOperationMatchAt(content, structure, operationStart, ["call", "bind", "apply"]); } -function rawSqlBindCloseParenIndex(structure, rawMatchEnd, statementEnd = structure.length) { - const operation = rawSqlMemberOperation(structure, rawMatchEnd); +function rawSqlBindCloseParenIndex(content, structure, rawMatchEnd, statementEnd = structure.length) { + const operation = rawSqlMemberOperation(content, structure, rawMatchEnd); if (operation?.kind !== "bind") return -1; - const methodOpenParenIndex = structure.indexOf("(", operation.methodNameEnd); + const methodOpenParenIndex = operation.openParenIndex ?? structure.indexOf("(", operation.methodNameEnd); if (methodOpenParenIndex === -1 || methodOpenParenIndex >= statementEnd) return -1; - const methodCloseParenIndex = findMatchingParen(structure, methodOpenParenIndex); + const methodCloseParenIndex = operation.closeParenIndex ?? findMatchingParen(structure, methodOpenParenIndex); return methodCloseParenIndex !== -1 && methodCloseParenIndex <= statementEnd ? methodCloseParenIndex : -1; } @@ -1214,7 +4415,148 @@ function isSimpleRawSqlBoundAliasInitializerPrefix(structure, initializerStart, return !/[;\n\r\u2028\u2029]/.test(prefix) && !/\b(?:const|let|var|await|return|throw|if|for|while|switch|class|function|import|export)\b/.test(prefix); } -function findRawSqlBoundExecutorAliases(structure) { +function isSimpleRawSqlExecutorAliasInitializerPrefix(structure, initializerStart, rawMatchStart) { + const prefix = structure.slice(initializerStart, rawMatchStart).trim(); + if (prefix.length === 0) return false; + + // 普通 raw executor alias 只接受静态 member chain,避免把条件表达式或函数返回值误判成 Prisma raw executor。 + const staticMemberChain = String.raw`(?:[A-Za-z_$][\w$]*|this)(?:\s*(?:(?:\.|\?\.)\s*[A-Za-z_$][\w$]*|(?:\?\.\s*)?\[\s*(?:"[^"]*"|'[^']*'|` + "`[^`]*`" + String.raw`)\s*\]))*`; + return new RegExp(String.raw`^${staticMemberChain}$`).test(prefix); +} + +function rawSqlPlainAliasInitializerEnd(structure, rawMatchEnd, statementEnd) { + const nextTokenIndex = skipWhitespaceForward(structure, rawMatchEnd); + if (nextTokenIndex === statementEnd) return statementEnd; + + const gapAfterAccess = structure.slice(rawMatchEnd, nextTokenIndex); + if (!hasLineTerminator(gapAfterAccess)) return -1; + if (isRawSqlBoundAliasExpressionContinuation(structure, nextTokenIndex)) return -1; + + // 无分号合法代码依靠 ASI 在换行处结束 initializer;alias.end 只覆盖 raw executor access 本身。 + return rawMatchEnd; +} + +function simpleRawSqlAliasInitializerEnd(structure, expressionEnd, statementEnd) { + const nextTokenIndex = skipWhitespaceForward(structure, expressionEnd); + if (nextTokenIndex === statementEnd) return statementEnd; + + const gapAfterExpression = structure.slice(expressionEnd, nextTokenIndex); + if (!hasLineTerminator(gapAfterExpression)) return -1; + if (isRawSqlBoundAliasExpressionContinuation(structure, nextTokenIndex)) return -1; + + return expressionEnd; +} + +function simpleStaticMemberChainMatchAt(content, structure, index) { + const receiverMatch = /^(?:[A-Za-z_$][\w$]*|this)\b/.exec(structure.slice(index)); + if (!receiverMatch) return null; + + let cursor = index + receiverMatch[0].length; + while (true) { + const nextCursor = skipWhitespaceForward(structure, cursor); + if (structure[nextCursor] !== "." && structure[nextCursor] !== "[" && !structure.startsWith("?.", nextCursor)) break; + + const propertyAccess = staticStaticMemberPropertyAccessMatchAt(content, structure, nextCursor); + if (!propertyAccess) break; + cursor = propertyAccess.end; + } + + return [structure.slice(index, cursor)]; +} + +function staticStaticMemberPropertyAccessMatchAt(content, structure, index) { + let propertyStart = index; + if (structure.startsWith("?.", index)) { + propertyStart = skipWhitespaceForward(structure, index + 2); + } else if (structure[index] === ".") { + propertyStart = skipWhitespaceForward(structure, index + 1); + } + + if (structure[propertyStart] === "[") { + const closeBracketIndex = findMatchingBracket(structure, propertyStart); + if (closeBracketIndex === -1) return null; + + const expressionStart = propertyStart + 1; + const expressionEnd = closeBracketIndex; + const values = staticJavaScriptExpressionValues( + content, + structure, + content.slice(expressionStart, expressionEnd), + expressionStart, + index, + new Set(), + 0 + ); + return values.length === 1 ? { index, end: closeBracketIndex + 1, value: values[0] } : null; + } + + if (propertyStart === index && structure[index] !== "." && !structure.startsWith("?.", index)) return null; + + const nameMatch = /^[A-Za-z_$][\w$]*/.exec(structure.slice(propertyStart)); + return nameMatch ? { index, end: propertyStart + nameMatch[0].length, value: nameMatch[0] } : null; +} + +function rawSqlExecutorAccessMatches(content, structure, startIndex = 0, endIndex = structure.length) { + const matches = []; + + for (let cursor = startIndex; cursor < endIndex; cursor += 1) { + if (structure[cursor] !== "." && structure[cursor] !== "[" && !structure.startsWith("?.", cursor)) continue; + + const match = staticPrismaPropertyAccessMatchAt(content, structure, cursor, rawSqlExecutorNames, cursor); + if (!match || match.end > endIndex) continue; + + matches.push(match); + cursor = match.end - 1; + } + + return matches; +} + +function completeRawSqlBoundExecutorAliasExpressionMatch(content, structure, sourceStart, sourceEnd) { + const unwrapped = unwrapCompleteParenthesizedJavaScriptExpression(structure, sourceStart, sourceEnd); + const expressionStart = skipWhitespaceForward(structure, unwrapped.start); + const expressionEnd = skipWhitespaceBackward(structure, unwrapped.end - 1) + 1; + if (expressionStart >= expressionEnd) return null; + + for (const rawMatch of rawSqlExecutorAccessMatches(content, structure, expressionStart, expressionEnd)) { + if (!isSimpleRawSqlBoundAliasInitializerPrefix(structure, expressionStart, rawMatch.index)) continue; + + const bindCloseParenIndex = rawSqlBindCloseParenIndex(content, structure, rawMatch.end, expressionEnd); + if (bindCloseParenIndex === -1) continue; + if (!isAtStaticWrapperExpressionEnd(structure, bindCloseParenIndex + 1, expressionEnd)) continue; + + return { kind: "rawExecutor", index: rawMatch.index, end: expressionEnd }; + } + + return null; +} + +function completeRawSqlPlainExecutorAliasExpressionMatch(content, structure, sourceStart, sourceEnd) { + const unwrapped = unwrapCompleteParenthesizedJavaScriptExpression(structure, sourceStart, sourceEnd); + const expressionStart = skipWhitespaceForward(structure, unwrapped.start); + const expressionEnd = skipWhitespaceBackward(structure, unwrapped.end - 1) + 1; + if (expressionStart >= expressionEnd) return null; + + for (const rawMatch of rawSqlExecutorAccessMatches(content, structure, expressionStart, expressionEnd)) { + if (!isSimpleRawSqlExecutorAliasInitializerPrefix(structure, expressionStart, rawMatch.index)) continue; + if (!isAtStaticWrapperExpressionEnd(structure, rawMatch.end, expressionEnd)) continue; + + return { kind: "rawExecutor", index: rawMatch.index, end: expressionEnd }; + } + + return null; +} + +function hasRawSqlExecutorAccess(content, structure) { + return rawSqlExecutorAccessMatches(content, structure).length > 0; +} + +function hasRawSqlExecutorAccessInRange(content, structure, startIndex, endIndex) { + const scanStructure = stripParenthesesAroundStaticMemberChains(content, structure); + return rawSqlExecutorAccessMatches(content, scanStructure, startIndex, endIndex).length > 0; +} + +function findRawSqlBoundExecutorAliases(content, structure) { const aliases = []; const declarationPattern = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\b(?:\s*:[^=;]+)?\s*=/g; @@ -1226,15 +4568,25 @@ function findRawSqlBoundExecutorAliases(structure) { const initializerStart = skipWhitespaceForward(structure, equalsIndex + 1); const statementEnd = findStatementEnd(structure, initializerStart); + const branchRawMatch = staticInitializerBranchMatch( + content, + structure, + initializerStart, + statementEnd, + completeRawSqlBoundExecutorAliasExpressionMatch + ); + if (branchRawMatch?.branch) { + // mixed ternary/logical 中任一分支返回 raw executor bind,都把 alias 视为 raw executor,防止 safe 分支掩盖危险分支。 + aliases.push({ name: declarationMatch[1], index: declarationMatch.index, end: statementEnd }); + continue; + } - for (const rawMatch of structure.slice(initializerStart, statementEnd).matchAll(rawSqlExecutorPattern())) { - if (typeof rawMatch.index !== "number") continue; - - const rawMatchStart = initializerStart + rawMatch.index; + for (const rawMatch of rawSqlExecutorAccessMatches(content, structure, initializerStart, statementEnd)) { + const rawMatchStart = rawMatch.index; if (!isSimpleRawSqlBoundAliasInitializerPrefix(structure, initializerStart, rawMatchStart)) continue; - const rawMatchEnd = rawMatchStart + rawMatch[0].length; - const bindCloseParenIndex = rawSqlBindCloseParenIndex(structure, rawMatchEnd, statementEnd); + const rawMatchEnd = rawMatch.end; + const bindCloseParenIndex = rawSqlBindCloseParenIndex(content, structure, rawMatchEnd, statementEnd); if (bindCloseParenIndex === -1) continue; const initializerEnd = rawSqlBoundAliasInitializerEnd(structure, bindCloseParenIndex, statementEnd); @@ -1253,96 +4605,657 @@ function findRawSqlBoundExecutorAliases(structure) { return aliases; } +function findRawSqlAssignedBoundExecutorAliases(content, structure) { + const aliases = []; + const assignmentPattern = /\b([A-Za-z_$][\w$]*)\b\s*=/g; + + for (const assignmentMatch of structure.matchAll(assignmentPattern)) { + if (typeof assignmentMatch.index !== "number") continue; + if (isMemberIdentifierReference(structure, assignmentMatch.index)) continue; + + const equalsIndex = structure.indexOf("=", assignmentMatch.index + assignmentMatch[0].lastIndexOf("=")); + if (equalsIndex === -1) continue; + if ("=>"].includes(structure[equalsIndex + 1] ?? "")) continue; + + const initializerStart = skipWhitespaceForward(structure, equalsIndex + 1); + const statementEnd = findStatementEnd(structure, initializerStart); + const branchRawMatch = staticInitializerBranchMatch( + content, + structure, + initializerStart, + statementEnd, + completeRawSqlBoundExecutorAliasExpressionMatch + ); + if (branchRawMatch?.branch) { + // 后续赋值也按同一规则 fail-closed,避免 `alias = ok ? raw.bind(db) : safe` 绕过。 + aliases.push({ name: assignmentMatch[1], index: assignmentMatch.index, end: statementEnd }); + continue; + } + + for (const rawMatch of rawSqlExecutorAccessMatches(content, structure, initializerStart, statementEnd)) { + const rawMatchStart = rawMatch.index; + if (!isSimpleRawSqlBoundAliasInitializerPrefix(structure, initializerStart, rawMatchStart)) continue; + + const rawMatchEnd = rawMatch.end; + const bindCloseParenIndex = rawSqlBindCloseParenIndex(content, structure, rawMatchEnd, statementEnd); + if (bindCloseParenIndex === -1) continue; + + const initializerEnd = rawSqlBoundAliasInitializerEnd(structure, bindCloseParenIndex, statementEnd); + if (initializerEnd === -1) continue; + + // 赋值 alias 覆盖 `let execRaw; execRaw = db.$executeRawUnsafe.bind(db)` 这类后续绑定。 + aliases.push({ name: assignmentMatch[1], index: assignmentMatch.index, end: initializerEnd }); + break; + } + } + + return aliases; +} + +function findRawSqlPlainExecutorAliases(content, structure) { + const aliases = []; + const declarationPattern = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\b(?:\s*:[^=;]+)?\s*=/g; + + for (const declarationMatch of structure.matchAll(declarationPattern)) { + if (typeof declarationMatch.index !== "number") continue; + + const equalsIndex = structure.indexOf("=", declarationMatch.index); + if (equalsIndex === -1) continue; + + const initializerStart = skipWhitespaceForward(structure, equalsIndex + 1); + const statementEnd = findStatementEnd(structure, initializerStart); + const branchRawMatch = staticInitializerBranchMatch( + content, + structure, + initializerStart, + statementEnd, + completeRawSqlPlainExecutorAliasExpressionMatch + ); + if (branchRawMatch?.branch) { + // 普通 raw executor alias 同样不能被 `safe || db.$executeRawUnsafe` 这类逻辑分支隐藏。 + aliases.push({ name: declarationMatch[1], index: declarationMatch.index, end: statementEnd }); + continue; + } + + for (const rawMatch of rawSqlExecutorAccessMatches(content, structure, initializerStart, statementEnd)) { + const rawMatchStart = rawMatch.index; + if (!isSimpleRawSqlExecutorAliasInitializerPrefix(structure, initializerStart, rawMatchStart)) continue; + + const rawMatchEnd = rawMatch.end; + const initializerEnd = rawSqlPlainAliasInitializerEnd(structure, rawMatchEnd, statementEnd); + if (initializerEnd === -1) continue; + + // 只跟踪同文件的简单 raw executor alias;复杂重赋值/跨函数数据流继续交给后续 review 扩展。 + aliases.push({ + name: declarationMatch[1], + index: declarationMatch.index, + end: initializerEnd + }); + break; + } + } + + return aliases; +} + +function findRawSqlAssignedPlainExecutorAliases(content, structure) { + const aliases = []; + const assignmentPattern = /\b([A-Za-z_$][\w$]*)\b\s*=/g; + + for (const assignmentMatch of structure.matchAll(assignmentPattern)) { + if (typeof assignmentMatch.index !== "number") continue; + if (isMemberIdentifierReference(structure, assignmentMatch.index)) continue; + + const equalsIndex = structure.indexOf("=", assignmentMatch.index + assignmentMatch[0].lastIndexOf("=")); + if (equalsIndex === -1) continue; + if ("=>"].includes(structure[equalsIndex + 1] ?? "")) continue; + + const initializerStart = skipWhitespaceForward(structure, equalsIndex + 1); + const statementEnd = findStatementEnd(structure, initializerStart); + const branchRawMatch = staticInitializerBranchMatch( + content, + structure, + initializerStart, + statementEnd, + completeRawSqlPlainExecutorAliasExpressionMatch + ); + if (branchRawMatch?.branch) { + // 赋值式 plain raw alias 覆盖 `let runRaw; runRaw = maybe || db.$executeRawUnsafe`。 + aliases.push({ name: assignmentMatch[1], index: assignmentMatch.index, end: statementEnd }); + continue; + } + + for (const rawMatch of rawSqlExecutorAccessMatches(content, structure, initializerStart, statementEnd)) { + const rawMatchStart = rawMatch.index; + if (!isSimpleRawSqlExecutorAliasInitializerPrefix(structure, initializerStart, rawMatchStart)) continue; + + const rawMatchEnd = rawMatch.end; + const initializerEnd = rawSqlPlainAliasInitializerEnd(structure, rawMatchEnd, statementEnd); + if (initializerEnd === -1) continue; + + // 赋值 alias 覆盖 `let execRaw; execRaw = db.$executeRawUnsafe` 这类后续绑定。 + aliases.push({ name: assignmentMatch[1], index: assignmentMatch.index, end: initializerEnd }); + break; + } + } + + return aliases; +} + +function destructuringParts(structure, startIndex, endIndex) { + const parts = []; + let partStart = startIndex; + let braceDepth = 0; + let bracketDepth = 0; + let parenDepth = 0; + + for (let cursor = startIndex; cursor < endIndex; cursor += 1) { + const char = structure[cursor]; + if (char === "{") braceDepth += 1; + if (char === "}") braceDepth -= 1; + if (char === "[") bracketDepth += 1; + if (char === "]") bracketDepth -= 1; + if (char === "(") parenDepth += 1; + if (char === ")") parenDepth -= 1; + if (char !== "," || braceDepth !== 0 || bracketDepth !== 0 || parenDepth !== 0) continue; + + parts.push({ start: partStart, end: cursor }); + partStart = cursor + 1; + } + + parts.push({ start: partStart, end: endIndex }); + return parts; +} + +function destructuredAliasNamesForStaticKeys(content, structure, bindingStart, bindingEnd, keys, beforeIndex) { + const names = []; + const allowedKeys = new Set(keys); + + for (const part of destructuringParts(structure, bindingStart, bindingEnd)) { + const key = staticDestructuringPropertyKeyValue(content, structure, part.start, part.end, beforeIndex); + if (!allowedKeys.has(key)) continue; + + const name = destructuringAliasName(structure, part.start, part.end); + if (name) names.push(name); + } + + return names; +} + +function staticDestructuringPropertyKeyValue(content, structure, partStart, partEnd, beforeIndex) { + const keyStart = skipWhitespaceForward(structure, partStart); + const keyEnd = destructuringPropertyKeyEnd(structure, keyStart, partEnd); + if (keyEnd === -1) return null; + + const expressionStart = structure[keyStart] === "[" ? keyStart + 1 : keyStart; + const expressionEnd = structure[keyStart] === "[" ? keyEnd - 1 : keyEnd; + const keySource = content.slice(expressionStart, expressionEnd); + if (structure[keyStart] !== "[" && /^[A-Za-z_$][\w$]*$/.test(keySource.trim())) return keySource.trim(); + + return staticJavaScriptExpressionValues(content, structure, keySource, expressionStart, beforeIndex, new Set(), 0)[0] ?? null; +} + +function destructuringPropertyKeyEnd(structure, keyStart, partEnd) { + if (keyStart >= partEnd) return -1; + + if (structure[keyStart] === "[") { + const closeBracketIndex = findMatchingBracket(structure, keyStart); + if (closeBracketIndex === -1 || closeBracketIndex >= partEnd) return -1; + const afterKey = skipWhitespaceForward(structure, closeBracketIndex + 1); + return structure[afterKey] === ":" ? closeBracketIndex + 1 : -1; + } + + const literalEnd = findStringLiteralEnd(structure, keyStart); + if ((structure[keyStart] === '"' || structure[keyStart] === "'" || structure[keyStart] === "`") && literalEnd !== -1 && literalEnd < partEnd) { + const afterKey = skipWhitespaceForward(structure, literalEnd + 1); + return structure[afterKey] === ":" ? literalEnd + 1 : -1; + } + + const identifierMatch = /^[A-Za-z_$][\w$]*/.exec(structure.slice(keyStart, partEnd)); + if (!identifierMatch) return -1; + + const keyEnd = keyStart + identifierMatch[0].length; + const afterKey = skipWhitespaceForward(structure, keyEnd); + if (structure[afterKey] === ":" || afterKey === partEnd) return keyEnd; + return -1; +} + +function destructuringAliasName(structure, partStart, partEnd) { + const keyStart = skipWhitespaceForward(structure, partStart); + const keyEnd = destructuringPropertyKeyEnd(structure, keyStart, partEnd); + if (keyEnd === -1) return null; + + const afterKey = skipWhitespaceForward(structure, keyEnd); + if (structure[afterKey] !== ":") { + const shorthandName = structure.slice(keyStart, keyEnd).trim(); + return /^[A-Za-z_$][\w$]*$/.test(shorthandName) ? shorthandName : null; + } + + const aliasStart = skipWhitespaceForward(structure, afterKey + 1); + const aliasMatch = /^[A-Za-z_$][\w$]*/.exec(structure.slice(aliasStart, partEnd)); + if (!aliasMatch) return null; + + const aliasEnd = aliasStart + aliasMatch[0].length; + const afterAlias = skipWhitespaceForward(structure, aliasEnd); + return afterAlias === partEnd ? aliasMatch[0] : null; +} + +function findRawSqlDestructuredExecutorAliases(content, structure) { + const aliases = []; + const declarationPattern = /\b(?:const|let|var)\s*\{/g; + + for (const declarationMatch of structure.matchAll(declarationPattern)) { + if (typeof declarationMatch.index !== "number") continue; + + const openBraceIndex = structure.indexOf("{", declarationMatch.index); + if (openBraceIndex === -1) continue; + const closeBraceIndex = findMatchingBrace(structure, openBraceIndex); + if (closeBraceIndex === -1) continue; + + const names = destructuredAliasNamesForStaticKeys( + content, + structure, + openBraceIndex + 1, + closeBraceIndex, + rawSqlExecutorNames, + declarationMatch.index + ); + if (names.length === 0) continue; + + let cursor = skipWhitespaceForward(structure, closeBraceIndex + 1); + if (structure[cursor] === ":") { + const equalsAfterType = structure.indexOf("=", cursor + 1); + const semicolonAfterType = structure.indexOf(";", cursor + 1); + if (equalsAfterType === -1 || (semicolonAfterType !== -1 && semicolonAfterType < equalsAfterType)) continue; + cursor = skipWhitespaceForward(structure, equalsAfterType + 1); + } else { + if (structure[cursor] !== "=") continue; + cursor = skipWhitespaceForward(structure, cursor + 1); + } + + const receiverMatch = simpleStaticMemberChainMatchAt(content, structure, cursor); + if (!receiverMatch) continue; + + const expressionEnd = cursor + receiverMatch[0].length; + const statementEnd = findStatementEnd(structure, cursor); + const aliasEnd = simpleRawSqlAliasInitializerEnd(structure, expressionEnd, statementEnd); + if (aliasEnd === -1) continue; + + // 解构 alias 只覆盖简单 receiver,避免把复杂运行时对象误判为 Prisma raw executor。 + for (const name of names) { + aliases.push({ name, index: declarationMatch.index, end: aliasEnd }); + } + } + + return aliases; +} + +function findRawSqlAssignedDestructuredExecutorAliases(content, structure) { + const aliases = []; + const assignmentPattern = /\(\s*\{/g; + + for (const assignmentMatch of structure.matchAll(assignmentPattern)) { + if (typeof assignmentMatch.index !== "number") continue; + + const openParenIndex = structure.indexOf("(", assignmentMatch.index); + if (openParenIndex === -1 || !isParenthesizedExpressionOpen(structure, openParenIndex)) continue; + + const openBraceIndex = structure.indexOf("{", openParenIndex); + if (openBraceIndex === -1) continue; + + const closeBraceIndex = findMatchingBrace(structure, openBraceIndex); + const closeParenIndex = findMatchingParen(structure, openParenIndex); + if (closeBraceIndex === -1 || closeParenIndex === -1 || closeBraceIndex > closeParenIndex) continue; + + const names = destructuredAliasNamesForStaticKeys( + content, + structure, + openBraceIndex + 1, + closeBraceIndex, + rawSqlExecutorNames, + assignmentMatch.index + ); + if (names.length === 0) continue; + + let cursor = skipWhitespaceForward(structure, closeBraceIndex + 1); + if (structure[cursor] !== "=") continue; + + cursor = skipWhitespaceForward(structure, cursor + 1); + const receiverMatch = simpleStaticMemberChainMatchAt(content, structure, cursor); + if (!receiverMatch) continue; + + const expressionEnd = cursor + receiverMatch[0].length; + if (skipWhitespaceForward(structure, expressionEnd) !== closeParenIndex) continue; + + const statementEnd = findStatementEnd(structure, openParenIndex); + const aliasEnd = simpleRawSqlAliasInitializerEnd(structure, closeParenIndex + 1, statementEnd); + if (aliasEnd === -1) continue; + + // 声明后的解构赋值 alias 覆盖 `let execRaw; ({ $executeRawUnsafe: execRaw } = db)`。 + for (const name of names) { + aliases.push({ name, index: assignmentMatch.index, end: aliasEnd }); + } + } + + return aliases; +} + function isMemberIdentifierReference(structure, identifierIndex) { const previousIndex = skipWhitespaceBackward(structure, identifierIndex - 1); return previousIndex >= 0 && structure[previousIndex] === "."; } -function rawSqlBoundAliasInvocationSources(content, structure) { +function firstArrayElementSource(content, structure, sourceStart, sourceEnd) { + return nthArrayElementSource(content, structure, { index: sourceStart, end: sourceEnd }, 0); +} + +function nthArrayElementSource(content, structure, arrayArgument, elementIndex) { + if (!arrayArgument) return null; + + const sourceStart = arrayArgument.index; + const sourceEnd = arrayArgument.end; + const arrayStart = skipWhitespaceForward(structure, sourceStart); + const arrayEnd = skipWhitespaceBackward(structure, sourceEnd - 1) + 1; + if (arrayStart >= arrayEnd || structure[arrayStart] !== "[") return null; + + const closeBracketIndex = findMatchingBracket(structure, arrayStart); + if (closeBracketIndex === -1 || closeBracketIndex !== arrayEnd - 1) return null; + + return nthCallArgumentSource(content, structure, arrayStart, closeBracketIndex, elementIndex); +} + +function rawSqlInvocationSourcesFromArrayArgument(content, structure, arrayArgument, beforeIndex) { + const sqlArgument = firstArrayElementSource(content, structure, arrayArgument.index, arrayArgument.end); + return sqlArgument ? [{ ...sqlArgument, beforeIndex }] : []; +} + +function rawSqlExecutorDirectAliases(content, structure) { + return findRawSqlBoundExecutorAliases(content, structure) + .concat(findRawSqlAssignedBoundExecutorAliases(content, structure)) + .concat(findRawSqlPlainExecutorAliases(content, structure)) + .concat(findRawSqlAssignedPlainExecutorAliases(content, structure)) + .concat(findRawSqlDestructuredExecutorAliases(content, structure)) + .concat(findRawSqlAssignedDestructuredExecutorAliases(content, structure)); +} + +function rawSqlExecutorAliases(content, structure) { + const directAliases = propagateStaticIdentifierAliases(structure, rawSqlExecutorDirectAliases(content, structure), (_sourceAlias, initializer) => ({ + name: initializer.name, + index: initializer.index, + end: initializer.end + })); + const wrappers = rawSqlExecutorWrapperSources(content, structure, directAliases); + const aliases = findRawSqlBoundExecutorAliases(content, structure) + .concat(findRawSqlAssignedBoundExecutorAliases(content, structure)) + .concat(findRawSqlPlainExecutorAliases(content, structure)) + .concat(findRawSqlAssignedPlainExecutorAliases(content, structure)) + .concat(findRawSqlDestructuredExecutorAliases(content, structure)) + .concat(findRawSqlAssignedDestructuredExecutorAliases(content, structure)) + .concat(findStaticWrapperExtractionAliases(content, structure, wrappers)); + + return propagateStaticIdentifierAliases(structure, aliases, (_sourceAlias, initializer) => ({ + name: initializer.name, + index: initializer.index, + end: initializer.end + })); +} + +function isRawSqlExecutorAliasTarget(structure, targetArgument, aliases) { + const aliasName = simpleIdentifierSourceName(structure, targetArgument.index, targetArgument.end); + if (!aliasName) return false; + + // Reflect.apply 只能使用调用点之前已建立的同文件 raw executor alias。 + return Boolean(latestStaticAliasRecordAt(structure, aliases, aliasName, targetArgument.index)); +} + +function isRawSqlExecutorTargetArgument(content, structure, targetArgument, aliases) { + return ( + targetArgument && + (hasRawSqlExecutorAccessInRange(content, structure, targetArgument.index, targetArgument.end) || + isRawSqlExecutorAliasTarget(structure, targetArgument, aliases)) + ); +} + +function rawSqlInvocationSourcesFromFunctionPrototypeArrayArgument(content, structure, arrayArgument, methodKind, beforeIndex) { + if (methodKind === "call") { + const sqlArgument = nthArrayElementSource(content, structure, arrayArgument, 1); + return sqlArgument ? [{ ...sqlArgument, beforeIndex }] : []; + } + + if (methodKind === "apply") { + const nestedArrayArgument = nthArrayElementSource(content, structure, arrayArgument, 1); + return nestedArrayArgument ? rawSqlInvocationSourcesFromArrayArgument(content, structure, nestedArrayArgument, beforeIndex) : []; + } + + return []; +} + +function rawSqlFunctionPrototypeInvocationSources(content, structure, builtinInvokerAliases) { + const sources = []; + const aliases = rawSqlExecutorAliases(content, structure); + const boundTargetAliases = findFunctionPrototypeBoundTargetAliases(content, structure); + + for (const invocation of functionPrototypeCallInvocationMatches(content, structure, builtinInvokerAliases)) { + const targetArgument = functionPrototypeEquivalentArgumentSource(content, structure, invocation, 0); + if (!isRawSqlExecutorTargetArgument(content, structure, targetArgument, aliases)) continue; + + if (invocation.methodKind === "call") { + // Function.prototype.call.call(raw, thisArg, sql, ...) 的 SQL 是外层调用第三参。 + const sqlArgument = functionPrototypeEquivalentArgumentSource(content, structure, invocation, 2); + if (sqlArgument) sources.push({ ...sqlArgument, beforeIndex: invocation.index }); + continue; + } + + // Function.prototype.apply.call(raw, thisArg, [sql, ...]) 的 SQL 在第三参数数组首项。 + const arrayArgument = functionPrototypeEquivalentArgumentSource(content, structure, invocation, 2); + if (arrayArgument) sources.push(...rawSqlInvocationSourcesFromArrayArgument(content, structure, arrayArgument, invocation.index)); + } + + for (const invocation of functionPrototypeBoundTargetInvocationMatches(content, structure, boundTargetAliases)) { + if (!isRawSqlExecutorTargetArgument(content, structure, invocation.targetArgument, aliases)) continue; + + if (invocation.methodKind === "call") { + // Function.prototype.call.bind(raw)(thisArg, sql, ...) 的 SQL 是绑定后调用的第二参。 + const sqlArgument = nthCallArgumentSource(content, structure, invocation.openParenIndex, invocation.closeParenIndex, 1); + if (sqlArgument) sources.push({ ...sqlArgument, beforeIndex: invocation.index }); + continue; + } + + // Function.prototype.apply.bind(raw)(thisArg, [sql, ...]) 的 SQL 在绑定后调用第二参数数组首项。 + const arrayArgument = nthCallArgumentSource(content, structure, invocation.openParenIndex, invocation.closeParenIndex, 1); + if (arrayArgument) sources.push(...rawSqlInvocationSourcesFromArrayArgument(content, structure, arrayArgument, invocation.index)); + } + + return sources; +} + +function rawSqlReflectApplyInvocationSources(content, structure, builtinInvokerAliases) { + const sources = []; + const aliases = rawSqlExecutorAliases(content, structure); + + for (const invocation of reflectApplyInvocationMatches(content, structure, builtinInvokerAliases)) { + const targetArgument = reflectApplyEquivalentArgumentSource(content, structure, invocation, 0); + if (!targetArgument) continue; + + const functionPrototypeMethod = functionPrototypeMethodKindFromArgument(content, structure, targetArgument, builtinInvokerAliases); + if (functionPrototypeMethod) { + const invokedFunctionArgument = reflectApplyEquivalentArgumentSource(content, structure, invocation, 1); + if (!isRawSqlExecutorTargetArgument(content, structure, invokedFunctionArgument, aliases)) continue; + + const arrayArgument = reflectApplyEquivalentArgumentSource(content, structure, invocation, 2); + if (!arrayArgument) continue; + + sources.push(...rawSqlInvocationSourcesFromFunctionPrototypeArrayArgument(content, structure, arrayArgument, functionPrototypeMethod, invocation.index)); + continue; + } + + if (!isRawSqlExecutorTargetArgument(content, structure, targetArgument, aliases)) continue; + + const arrayArgument = reflectApplyEquivalentArgumentSource(content, structure, invocation, 2); + if (!arrayArgument) continue; + + sources.push(...rawSqlInvocationSourcesFromArrayArgument(content, structure, arrayArgument, invocation.index)); + } + + return sources; +} + +function rawSqlInvocationSourcesFromOperation(content, structure, operation, beforeIndex) { + const sources = []; + if (!operation) return sources; + + if (operation.kind === "direct") { + const closeParenIndex = findMatchingParen(structure, operation.callStart); + if (closeParenIndex === -1) return sources; + const sqlArgument = nthCallArgumentSource(content, structure, operation.callStart, closeParenIndex, 0); + if (sqlArgument) sources.push({ ...sqlArgument, beforeIndex }); + return sources; + } + + if (operation.kind === "template") { + const templateEnd = findStringLiteralEnd(content, operation.templateStart); + if (templateEnd !== -1) { + sources.push({ + source: content.slice(operation.templateStart, templateEnd + 1), + index: operation.templateStart, + end: templateEnd + 1, + beforeIndex + }); + } + return sources; + } + + if (operation.kind !== "call" && operation.kind !== "bind" && operation.kind !== "apply") return sources; + + const methodOpenParenIndex = operation.openParenIndex ?? structure.indexOf("(", operation.methodNameEnd); + if (methodOpenParenIndex === -1) return sources; + const methodCloseParenIndex = operation.closeParenIndex ?? findMatchingParen(structure, methodOpenParenIndex); + if (methodCloseParenIndex === -1) return sources; + + if (operation.kind === "call") { + // .call(thisArg, sql, ...) 的 SQL 是第二个参数。 + const sqlArgument = nthCallArgumentSource(content, structure, methodOpenParenIndex, methodCloseParenIndex, 1); + if (sqlArgument) sources.push({ ...sqlArgument, beforeIndex }); + return sources; + } + + if (operation.kind === "apply") { + // .apply(thisArg, [sql, ...]) 的 SQL 在参数数组首项,和 Reflect.apply 共享同一提取语义。 + const arrayArgument = nthCallArgumentSource(content, structure, methodOpenParenIndex, methodCloseParenIndex, 1); + return arrayArgument ? rawSqlInvocationSourcesFromArrayArgument(content, structure, arrayArgument, beforeIndex) : sources; + } + + let boundCallStart = skipWhitespaceForward(structure, methodCloseParenIndex + 1); + if (structure.startsWith("?.", boundCallStart)) { + boundCallStart = skipWhitespaceForward(structure, boundCallStart + 2); + } + + if (structure[boundCallStart] !== "(") return sources; + const boundCloseParenIndex = findMatchingParen(structure, boundCallStart); + if (boundCloseParenIndex === -1) return sources; + const sqlArgument = nthCallArgumentSource(content, structure, boundCallStart, boundCloseParenIndex, 0); + if (sqlArgument) sources.push({ ...sqlArgument, beforeIndex }); + return sources; +} + +function rawSqlSourceMatcher(aliases = []) { + return (content, structure, sourceStart, sourceEnd) => { + const accessMatches = rawSqlExecutorAccessMatches(content, structure, sourceStart, sourceEnd); + + for (const rawMatch of accessMatches) { + if (!isSimpleRawSqlExecutorAliasInitializerPrefix(structure, sourceStart, rawMatch.index)) continue; + + const bindCloseParenIndex = rawSqlBindCloseParenIndex(content, structure, rawMatch.end, sourceEnd); + if (bindCloseParenIndex !== -1 && isAtStaticWrapperExpressionEnd(structure, bindCloseParenIndex + 1, sourceEnd)) { + return { kind: "rawExecutor", index: rawMatch.index, end: bindCloseParenIndex + 1 }; + } + + if (isAtStaticWrapperExpressionEnd(structure, rawMatch.end, sourceEnd)) { + return { kind: "rawExecutor", index: rawMatch.index, end: rawMatch.end }; + } + } + + return staticIdentifierAliasSourceMatchAt(structure, aliases, sourceStart, sourceEnd, sourceStart); + }; +} + +function rawSqlExecutorWrapperSources(content, structure, aliases = []) { + return staticWrapperSources(content, structure, rawSqlSourceMatcher(aliases)); +} + +function rawSqlWrappedExecutorInvocationSources(content, structure) { + const sources = []; + const directAliases = propagateStaticIdentifierAliases(structure, rawSqlExecutorDirectAliases(content, structure), (_sourceAlias, initializer) => ({ + name: initializer.name, + index: initializer.index, + end: initializer.end + })); + + for (const usage of staticWrapperSourceUsageMatches(content, structure, rawSqlExecutorWrapperSources(content, structure, directAliases))) { + const operation = rawSqlMemberOperation(content, structure, usage.end); + sources.push(...rawSqlInvocationSourcesFromOperation(content, structure, operation, usage.index)); + } + + return sources; +} + +function rawSqlAliasInvocationSources(content, structure, aliases) { const sources = []; - const aliases = findRawSqlBoundExecutorAliases(structure); for (const alias of aliases) { - const aliasCallPattern = new RegExp(String.raw`\b${escapeRegExp(alias.name)}\b\s*(?:<[^>()` + "`" + String.raw`]*>)?\s*`, "g"); + const aliasCallPattern = new RegExp(String.raw`(?:^|[^\w$])(${escapeRegExp(alias.name)})\b\s*(?:<[^>()` + "`" + String.raw`]*>)?\s*`, "g"); for (const aliasMatch of structure.matchAll(aliasCallPattern)) { if (typeof aliasMatch.index !== "number" || aliasMatch.index <= alias.end) continue; - if (isMemberIdentifierReference(structure, aliasMatch.index)) continue; + const aliasIndex = aliasMatch.index + aliasMatch[0].indexOf(alias.name); + if (aliasIndex <= alias.end) continue; + if (isMemberIdentifierReference(structure, aliasIndex)) continue; - let callStart = skipWhitespaceForward(structure, aliasMatch.index + aliasMatch[0].length); - if (structure.startsWith("?.", callStart)) { - callStart = skipWhitespaceForward(structure, callStart + 2); - } - - if (structure[callStart] !== "(") continue; - - const closeParenIndex = findMatchingParen(structure, callStart); - if (closeParenIndex === -1) continue; - - const sqlArgument = nthCallArgumentSource(content, structure, callStart, closeParenIndex, 0); - if (sqlArgument) sources.push({ ...sqlArgument, beforeIndex: aliasMatch.index }); + const operationStart = skipWhitespaceForward(structure, aliasIndex + alias.name.length); + const operation = rawSqlMemberOperation(content, structure, operationStart); + sources.push(...rawSqlInvocationSourcesFromOperation(content, structure, operation, aliasIndex)); } } return sources; } +function rawSqlBoundAliasInvocationSources(content, structure) { + return rawSqlAliasInvocationSources( + content, + structure, + findRawSqlBoundExecutorAliases(content, structure).concat(findRawSqlAssignedBoundExecutorAliases(content, structure)) + ); +} + +function rawSqlPlainAliasInvocationSources(content, structure) { + return rawSqlAliasInvocationSources( + content, + structure, + findRawSqlPlainExecutorAliases(content, structure) + .concat(findRawSqlAssignedPlainExecutorAliases(content, structure)) + .concat(findRawSqlDestructuredExecutorAliases(content, structure)) + .concat(findRawSqlAssignedDestructuredExecutorAliases(content, structure)) + ); +} + function rawSqlInvocationSources(content, structure = rawSqlStructure(content)) { + const scanStructure = stripParenthesesAroundStaticMemberChains(content, structure); const sources = []; + const builtinInvokerAliases = findStaticBuiltinInvokerAliases(content, scanStructure); - for (const match of structure.matchAll(rawSqlExecutorPattern())) { - if (typeof match.index !== "number") continue; - let callStart = skipWhitespaceForward(structure, match.index + match[0].length); - const operation = rawSqlMemberOperation(structure, callStart); - - if (operation?.kind === "direct") { - const closeParenIndex = findMatchingParen(structure, operation.callStart); - if (closeParenIndex === -1) continue; - const sqlArgument = nthCallArgumentSource(content, structure, operation.callStart, closeParenIndex, 0); - if (sqlArgument) sources.push({ ...sqlArgument, beforeIndex: match.index }); - continue; - } - - if (operation?.kind === "template") { - const templateEnd = findStringLiteralEnd(content, operation.templateStart); - if (templateEnd !== -1) { - sources.push({ - source: content.slice(operation.templateStart, templateEnd + 1), - index: operation.templateStart, - end: templateEnd + 1, - beforeIndex: match.index - }); - } - continue; - } - - if (operation?.kind !== "call" && operation?.kind !== "bind") continue; - - const methodOpenParenIndex = structure.indexOf("(", operation.methodNameEnd); - if (methodOpenParenIndex === -1) continue; - const methodCloseParenIndex = findMatchingParen(structure, methodOpenParenIndex); - if (methodCloseParenIndex === -1) continue; - - if (operation.kind === "call") { - // .call(thisArg, sql, ...) 的 SQL 是第二个参数。 - const sqlArgument = nthCallArgumentSource(content, structure, methodOpenParenIndex, methodCloseParenIndex, 1); - if (sqlArgument) sources.push({ ...sqlArgument, beforeIndex: match.index }); - continue; - } - - let boundCallStart = skipWhitespaceForward(structure, methodCloseParenIndex + 1); - if (structure.startsWith("?.", boundCallStart)) { - boundCallStart = skipWhitespaceForward(structure, boundCallStart + 2); - } - - if (structure[boundCallStart] !== "(") continue; - const boundCloseParenIndex = findMatchingParen(structure, boundCallStart); - if (boundCloseParenIndex === -1) continue; - const sqlArgument = nthCallArgumentSource(content, structure, boundCallStart, boundCloseParenIndex, 0); - if (sqlArgument) sources.push({ ...sqlArgument, beforeIndex: match.index }); + for (const match of rawSqlExecutorAccessMatches(content, scanStructure)) { + let callStart = skipWhitespaceForward(scanStructure, match.end); + const operation = rawSqlMemberOperation(content, scanStructure, callStart); + sources.push(...rawSqlInvocationSourcesFromOperation(content, scanStructure, operation, match.index)); } - return sources.concat(rawSqlBoundAliasInvocationSources(content, structure)); + return sources.concat( + rawSqlReflectApplyInvocationSources(content, scanStructure, builtinInvokerAliases), + rawSqlFunctionPrototypeInvocationSources(content, scanStructure, builtinInvokerAliases), + rawSqlWrappedExecutorInvocationSources(content, scanStructure), + rawSqlAliasInvocationSources(content, scanStructure, rawSqlExecutorAliases(content, scanStructure)) + ); } function simpleIdentifierSourceName(structure, sourceStart, sourceEnd) { @@ -1395,16 +5308,27 @@ function hasForbiddenRawJobStatusWrite(filePath, content) { function hasRawAuditLogMutation(sqlSource, options = {}) { const sql = normalizeRawSqlScanSource(sqlSource); const auditLogTable = sqlTableReferencePattern("AuditLog"); - const auditLogAppendOnlyFunction = sqlFunctionReferencePattern(auditLogAppendOnlyFunctionName); + const auditLogAppendOnlyFunction = sqlSpecificFunctionReferencePattern(auditLogAppendOnlyFunctionName); + const auditLogAppendOnlyFunctionList = sqlFunctionListContainingPattern(auditLogAppendOnlyFunction); const identifier = sqlBareIdentifierPattern(); const triggerIdentifier = sqlTriggerIdentifierPattern(); - const createsAuditLogAppendOnlyFunction = new RegExp( - String.raw`\bCREATE\s+(?:OR\s+REPLACE\s+)?FUNCTION\s+${auditLogAppendOnlyFunction}\b`, + // PostgreSQL UPDATE 允许目标表后带 `*` 继承标记;AuditLog append-only 扫描必须覆盖该合法语法。 + const createsPlainAuditLogAppendOnlyFunction = new RegExp( + String.raw`\bCREATE\s+FUNCTION\s+${auditLogAppendOnlyFunction}\b`, + "i" + ).test(sql); + const createsOrReplacesAuditLogAppendOnlyFunction = new RegExp( + String.raw`\bCREATE\s+OR\s+REPLACE\s+FUNCTION\s+${auditLogAppendOnlyFunction}\b`, "i" ).test(sql); return ( - new RegExp(String.raw`\bUPDATE\s+(?:ONLY\s+)?${auditLogTable}(?:\s+(?:AS\s+)?${identifier})?\s+SET\b`, "i").test(sql) || + new RegExp(String.raw`\bUPDATE\s+(?:ONLY\s+)?${auditLogTable}(?:\s*\*)?(?:\s+(?:AS\s+)?${identifier})?\s+SET\b`, "i").test(sql) || new RegExp(String.raw`\bDELETE\s+FROM\s+(?:ONLY\s+)?${auditLogTable}\b`, "i").test(sql) || + new RegExp( + String.raw`\bINSERT\s+INTO\s+(?:ONLY\s+)?${auditLogTable}\b[\s\S]*?\bON\s+CONFLICT\b[\s\S]*?\bDO\s+UPDATE\b`, + "i" + ).test(sql) || + new RegExp(String.raw`\bMERGE\s+INTO\s+(?:ONLY\s+)?${auditLogTable}(?:\s+(?:AS\s+)?${identifier})?\b`, "i").test(sql) || new RegExp(sqlTruncateTableListPattern(auditLogTable), "i").test(sql) || new RegExp(sqlDropTableListPattern(auditLogTable), "i").test(sql) || new RegExp(String.raw`\bDROP\s+TRIGGER\s+(?:IF\s+EXISTS\s+)?${triggerIdentifier}\s+ON\s+(?:ONLY\s+)?${auditLogTable}\b`, "i").test(sql) || @@ -1412,16 +5336,17 @@ function hasRawAuditLogMutation(sqlSource, options = {}) { String.raw`\bALTER\s+TABLE\s+(?:IF\s+EXISTS\s+)?(?:ONLY\s+)?${auditLogTable}\s+(?:DISABLE\s+(?:TRIGGER|RULE)\b|DROP\b|RENAME\b)`, "i" ).test(sql) || - new RegExp(String.raw`\bDROP\s+FUNCTION\s+(?:IF\s+EXISTS\s+)?${auditLogAppendOnlyFunction}\b`, "i").test(sql) || - (!options.allowAuditLogAppendOnlyFunctionCreate && createsAuditLogAppendOnlyFunction) || - new RegExp(String.raw`\bALTER\s+FUNCTION\s+${auditLogAppendOnlyFunction}\b`, "i").test(sql) + new RegExp(String.raw`\bDROP\s+(?:FUNCTION|ROUTINE)\s+(?:IF\s+EXISTS\s+)?${auditLogAppendOnlyFunctionList}(?=\s|;|$)`, "i").test( + sql + ) || + (!options.allowAuditLogAppendOnlyFunctionPlainCreate && createsPlainAuditLogAppendOnlyFunction) || + (!options.allowAuditLogAppendOnlyFunctionCreateOrReplace && createsOrReplacesAuditLogAppendOnlyFunction) || + new RegExp(String.raw`\bALTER\s+(?:FUNCTION|ROUTINE)\s+(?:IF\s+EXISTS\s+)?${auditLogAppendOnlyFunction}(?=\s|;|$)`, "i").test(sql) ); } function hasForbiddenRawAuditLogMutation(filePath, content, structure) { - const auditSqlOptions = { - allowAuditLogAppendOnlyFunctionCreate: allowsAuditLogAppendOnlyFunctionDefinition(filePath) - }; + const auditSqlOptions = auditLogAppendOnlyFunctionCreateOptions(filePath, content); if (filePath.endsWith(".sql")) return hasRawAuditLogMutation(content, auditSqlOptions); @@ -1494,13 +5419,331 @@ function hasForbiddenMigrationGuardSetter(content) { return /set_config\s*\(|SET\s+LOCAL/i.test(sql); } +function hasStateTransitionSpecifier(value) { + return /state-transition(?:\/index\.js)?/.test(value); +} + +function unwrapCompleteParenthesizedJavaScriptExpression(structure, sourceStart, sourceEnd) { + let trimmedStart = skipWhitespaceForward(structure, sourceStart); + let trimmedEnd = skipWhitespaceBackward(structure, sourceEnd - 1) + 1; + + while (trimmedStart < trimmedEnd && structure[trimmedStart] === "(") { + const closeParenIndex = findMatchingParen(structure, trimmedStart); + if (closeParenIndex !== trimmedEnd - 1) break; + + const innerStart = skipWhitespaceForward(structure, trimmedStart + 1); + const innerEnd = skipWhitespaceBackward(structure, closeParenIndex - 1) + 1; + if (innerStart >= innerEnd) break; + + // 只剥离完整包住表达式的括号,避免改变 ("a") + b 这类拼接分片边界。 + trimmedStart = innerStart; + trimmedEnd = innerEnd; + } + + return { start: trimmedStart, end: trimmedEnd }; +} + +function decodeStaticTemplateLiteralExpressionValue(content, structure, sourceStart, sourceEnd, beforeIndex, seenVariables, depth) { + if (content[sourceStart] !== "`") return null; + + const templateEnd = findStringLiteralEnd(content, sourceStart); + if (templateEnd !== sourceEnd - 1) return null; + + let output = ""; + let cursor = sourceStart + 1; + + while (cursor < templateEnd) { + if (content[cursor] === "\\") { + if (cursor + 1 < templateEnd) { + output += decodeJavaScriptStringLiteralBody(content.slice(cursor, cursor + 2)); + cursor += 2; + continue; + } + return null; + } + + if (content[cursor] === "$" && content[cursor + 1] === "{") { + const expressionStart = cursor + 2; + const expressionEnd = findMatchingBrace(content, cursor + 1); + if (expressionEnd === -1 || expressionEnd > templateEnd) return null; + const expressionStructure = + structure.slice(0, expressionStart) + content.slice(expressionStart, expressionEnd) + structure.slice(expressionEnd); + + const values = staticJavaScriptExpressionValues( + content, + expressionStructure, + content.slice(expressionStart, expressionEnd), + expressionStart, + beforeIndex, + seenVariables, + depth + 1 + ); + if (values.length !== 1) return null; + + output += values[0]; + cursor = expressionEnd + 1; + continue; + } + + output += content[cursor]; + cursor += 1; + } + + return output; +} + +function staticJavaScriptExpressionValues(content, structure, source, sourceStart, beforeIndex, seenVariables, depth) { + if (depth >= rawSqlVariableResolutionMaxDepth) return []; + + const { start: trimmedStart, end: trimmedEnd } = unwrapCompleteParenthesizedJavaScriptExpression( + structure, + sourceStart, + sourceStart + source.length + ); + if (trimmedStart >= trimmedEnd) return []; + + const expression = content.slice(trimmedStart, trimmedEnd); + const literalValue = decodeStaticJavaScriptStringLiteralSource(expression); + if (literalValue !== null) return [literalValue]; + + const templateValue = decodeStaticTemplateLiteralExpressionValue(content, structure, trimmedStart, trimmedEnd, beforeIndex, seenVariables, depth); + if (templateValue !== null) return [templateValue]; + + const identifierName = simpleIdentifierSourceName(structure, trimmedStart, trimmedEnd); + if (identifierName) { + return staticStringVariableInitializers(content, structure, identifierName, beforeIndex, seenVariables, depth + 1); + } + + const expressionStructure = structure.slice(trimmedStart, trimmedEnd); + let parts = []; + let partStart = 0; + let parenDepth = 0; + let braceDepth = 0; + let bracketDepth = 0; + + for (let cursor = 0; cursor < expressionStructure.length; cursor += 1) { + const char = expressionStructure[cursor]; + if (char === "(") parenDepth += 1; + if (char === ")") parenDepth -= 1; + if (char === "{") braceDepth += 1; + if (char === "}") braceDepth -= 1; + if (char === "[") bracketDepth += 1; + if (char === "]") bracketDepth -= 1; + if (char !== "+" || parenDepth !== 0 || braceDepth !== 0 || bracketDepth !== 0) continue; + + parts.push({ start: trimmedStart + partStart, end: trimmedStart + cursor }); + partStart = cursor + 1; + } + + if (parts.length === 0) return []; + parts.push({ start: trimmedStart + partStart, end: trimmedEnd }); + + const values = []; + for (const part of parts) { + const partSource = content.slice(part.start, part.end); + const partValues = staticJavaScriptExpressionValues(content, structure, partSource, part.start, beforeIndex, seenVariables, depth + 1); + if (partValues.length !== 1) return []; + values.push(partValues[0]); + } + + // 只折叠全静态字符串拼接;任何动态片段都保持未知,不影响其它动态 import。 + return [values.join("")]; +} + +function staticStringVariableInitializers(content, structure, variableName, beforeIndex, seenVariables = new Set(), depth = 0) { + if (depth >= rawSqlVariableResolutionMaxDepth || seenVariables.has(variableName)) return []; + + const values = []; + const nestedSeenVariables = new Set(seenVariables); + nestedSeenVariables.add(variableName); + const initializerPatterns = [ + new RegExp(String.raw`\b(?:const|let|var)\s+${escapeRegExp(variableName)}\b(?:\s*:[^=;]+)?\s*=`, "g"), + new RegExp(String.raw`\b${escapeRegExp(variableName)}\b\s*=`, "g") + ]; + + for (const initializerPattern of initializerPatterns) { + for (const match of structure.matchAll(initializerPattern)) { + if (typeof match.index !== "number" || match.index >= beforeIndex) continue; + + const variableOffset = match[0].indexOf(variableName); + const variableIndex = match.index + variableOffset; + if (initializerPattern !== initializerPatterns[0] && isMemberIdentifierReference(structure, variableIndex)) continue; + if (initializerPattern !== initializerPatterns[0] && /\b(?:const|let|var)\s*$/.test(structure.slice(Math.max(0, match.index - 16), variableIndex))) { + continue; + } + + const equalsIndex = structure.indexOf("=", match.index + match[0].lastIndexOf("=")); + if (equalsIndex === -1 || equalsIndex >= beforeIndex) continue; + if ("=>"].includes(structure[equalsIndex + 1] ?? "")) continue; + + // 静态 key alias 既支持声明初始化,也支持调用点前的简单后续赋值。 + const initializerStart = skipWhitespaceForward(structure, equalsIndex + 1); + const initializerEnd = findStatementEnd(structure, initializerStart); + values.push( + ...staticJavaScriptExpressionValues( + content, + structure, + content.slice(initializerStart, initializerEnd), + initializerStart, + beforeIndex, + nestedSeenVariables, + depth + ) + ); + } + } + + return values; +} + +function hasStateTransitionVariableImport(content) { + const structure = maskStringLiterals(maskCommentsOutsideStringLiterals(content)); + const dynamicImportPattern = /\b(?:import|require)\s*\(/g; + + for (const match of structure.matchAll(dynamicImportPattern)) { + if (typeof match.index !== "number") continue; + const openParenIndex = structure.indexOf("(", match.index); + const closeParenIndex = openParenIndex === -1 ? -1 : findMatchingParen(structure, openParenIndex); + if (closeParenIndex === -1) continue; + + const argumentStart = skipWhitespaceForward(structure, openParenIndex + 1); + const argumentEnd = skipWhitespaceBackward(structure, closeParenIndex - 1) + 1; + if (argumentStart >= argumentEnd) continue; + + // 动态 import/require 的实参统一复用静态 JS 表达式折叠,覆盖 identifier、递归 alias 与直接字符串拼接。 + if ( + staticJavaScriptExpressionValues( + content, + structure, + content.slice(argumentStart, argumentEnd), + argumentStart, + match.index, + new Set(), + 0 + ).some(hasStateTransitionSpecifier) + ) { + return true; + } + } + + return hasStateTransitionRequireAliasImport(content, structure); +} + +function findRequireAliases(structure) { + const aliases = []; + const destructuringPattern = /\b(?:const|let|var)\s*\{\s*require\s*:\s*([A-Za-z_$][\w$]*)\s*\}\s*=\s*module\b/g; + const patterns = [ + /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\b(?:\s*:[^=;]+)?\s*=\s*require\b/g, + /\b([A-Za-z_$][\w$]*)\b\s*=\s*require\b/g + ]; + + for (const match of structure.matchAll(destructuringPattern)) { + if (typeof match.index !== "number") continue; + + const moduleIndex = match.index + match[0].lastIndexOf("module"); + const afterModule = skipWhitespaceForward(structure, moduleIndex + "module".length); + const statementEnd = findStatementEnd(structure, moduleIndex); + if (afterModule !== statementEnd && !hasLineTerminator(structure.slice(afterModule, statementEnd))) continue; + + // CommonJS module.require 的解构 alias 只接受简单同文件声明。 + aliases.push({ name: match[1], index: match.index, end: afterModule }); + } + + for (const pattern of patterns) { + for (const match of structure.matchAll(pattern)) { + if (typeof match.index !== "number") continue; + if (isMemberIdentifierReference(structure, match.index + match[0].indexOf(match[1]))) continue; + + const requireIndex = match.index + match[0].lastIndexOf("require"); + const afterRequire = skipWhitespaceForward(structure, requireIndex + "require".length); + const statementEnd = findStatementEnd(structure, requireIndex); + if (afterRequire !== statementEnd && !hasLineTerminator(structure.slice(afterRequire, statementEnd))) continue; + + // require alias 只跟踪简单声明/赋值,复杂初始化器不进入 import 边界判断。 + aliases.push({ name: match[1], index: match.index, end: afterRequire }); + } + } + + return aliases; +} + +function hasStateTransitionRequireAliasImport(content, structure) { + for (const alias of findRequireAliases(structure)) { + const aliasCallPattern = new RegExp(String.raw`\b${escapeRegExp(alias.name)}\b\s*\(`, "g"); + + for (const match of structure.matchAll(aliasCallPattern)) { + if (typeof match.index !== "number" || match.index <= alias.end) continue; + if (isMemberIdentifierReference(structure, match.index)) continue; + + const openParenIndex = structure.indexOf("(", match.index + match[0].length - 1); + const closeParenIndex = openParenIndex === -1 ? -1 : findMatchingParen(structure, openParenIndex); + if (closeParenIndex === -1) continue; + + const argument = nthCallArgumentSource(content, structure, openParenIndex, closeParenIndex, 0); + if (!argument) continue; + + if ( + staticJavaScriptExpressionValues(content, structure, argument.source, argument.index, match.index, new Set(), 0).some( + hasStateTransitionSpecifier + ) + ) { + return true; + } + } + } + + return false; +} + +function javaScriptStringLiteralSourcePattern() { + return String.raw`(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|` + "`(?:\\\\.|[^`\\\\])*`" + String.raw`)`; +} + +function staticModuleSpecifierValues(content) { + const values = []; + const structure = maskCommentsOutsideStringLiterals(content); + const literalSource = javaScriptStringLiteralSourcePattern(); + const patterns = [ + new RegExp(String.raw`\bimport\s+(${literalSource})`, "g"), + new RegExp(String.raw`\bfrom\s+(${literalSource})`, "g") + ]; + + for (const pattern of patterns) { + for (const match of structure.matchAll(pattern)) { + if (typeof match.index !== "number") continue; + const literalOffset = match[0].lastIndexOf(match[1]); + const literalStart = match.index + literalOffset; + const decodedValue = decodeStaticJavaScriptStringLiteralSource(content.slice(literalStart, literalStart + match[1].length)); + if (decodedValue !== null) values.push(decodedValue); + } + } + + return values; +} + +function hasStaticStateTransitionSpecifier(content) { + return staticModuleSpecifierValues(content).some(hasStateTransitionSpecifier); +} + function hasStateTransitionServiceImport(content) { - return /from\s+["'][^"']*state-transition(?:\/index\.js)?["']|StateTransitionService/.test(content); + // state-transition 只能由白名单路径直接耦合;静态、动态与 CommonJS 字符串入口都要按同一路径语义拦截。 + return ( + hasStaticStateTransitionSpecifier(content) || + /\bimport\s*\(\s*(?:["'][^"']*state-transition(?:\/index\.js)?["']|`[^`]*state-transition(?:\/index\.js)?`)\s*\)/.test( + content + ) || + /\brequire\s*\(\s*(?:["'][^"']*state-transition(?:\/index\.js)?["']|`[^`]*state-transition(?:\/index\.js)?`)\s*\)/.test( + content + ) || + hasStateTransitionVariableImport(content) || + /StateTransitionService/.test(content) + ); } function isAllowedStateTransitionImportPath(filePath) { // jobs 测试需要 StateTransitionService 创建受 guard 保护的版本夹具;这不是生产调用入口。 if (stateTransitionImportFixtureTests.has(filePath)) return true; + if (allowedStateTransitionImportPaths.has(filePath)) return true; return allowedStateTransitionImportPrefixes.some((prefix) => filePath.startsWith(prefix)); } @@ -1513,10 +5756,107 @@ function isAllowedS1AnchorPath(filePath) { ); } -function hasJobStatusWrite(content) { +function hasPrismaModelMethodCallExcluding(content, models, methods, excludeCallBody) { + const scanContent = stripStringLiteralsExceptPropertyKeys(maskCommentsOutsideStringLiterals(content)); + for (const match of prismaModelMethodCallMatchesInStructure(content, scanContent, models, methods)) { + const openParenIndex = scanContent[match.callStart] === "(" ? match.callStart : scanContent.indexOf("(", match.callStart); + if (openParenIndex === -1) return true; + + const closeParenIndex = findMatchingParen(scanContent, openParenIndex); + if (closeParenIndex === -1) return true; + + const callBody = scanContent.slice(openParenIndex, closeParenIndex + 1); + if (!excludeCallBody(callBody)) return true; + } + return false; +} + +function isSimpleIdentifierKey(structure, partStart, partEnd, expectedKey) { + const keyStart = skipWhitespaceForward(structure, partStart); + const keyEnd = destructuringPropertyKeyEnd(structure, keyStart, partEnd); + if (keyEnd === -1) return false; + if (structure.slice(keyStart, keyEnd).trim() !== expectedKey) return false; + return structure[skipWhitespaceForward(structure, keyEnd)] === ":"; +} + +function objectPropertyValueSpan(structure, partStart, partEnd) { + const keyStart = skipWhitespaceForward(structure, partStart); + const keyEnd = destructuringPropertyKeyEnd(structure, keyStart, partEnd); + if (keyEnd === -1) return null; + + const colonIndex = skipWhitespaceForward(structure, keyEnd); + if (structure[colonIndex] !== ":") return null; + + const valueStart = skipWhitespaceForward(structure, colonIndex + 1); + const valueEnd = skipWhitespaceBackward(structure, partEnd - 1) + 1; + return valueStart < valueEnd ? { start: valueStart, end: valueEnd } : null; +} + +function soleObjectPropertyValueSpan(structure, objectStart, objectEnd, expectedKey) { + if (structure[objectStart] !== "{" || objectEnd <= objectStart) return null; + const closeBraceIndex = findMatchingBrace(structure, objectStart); + if (closeBraceIndex === -1 || closeBraceIndex !== objectEnd - 1) return null; + + const parts = destructuringParts(structure, objectStart + 1, closeBraceIndex).filter( + (part) => skipWhitespaceForward(structure, part.start) < skipWhitespaceBackward(structure, part.end - 1) + 1 + ); + if (parts.length !== 1) return null; + + const [part] = parts; + if (!isSimpleIdentifierKey(structure, part.start, part.end, expectedKey)) return null; + return objectPropertyValueSpan(structure, part.start, part.end); +} + +function isRunIdStartsWithValue(structure, valueStart, valueEnd) { + const value = structure.slice(valueStart, valueEnd).trim(); + return value === "runId"; +} + +function isAllowedJobDeleteCleanupCallBody(filePath, callBody) { + if (!isTestFile(filePath)) return false; + + const callStart = skipWhitespaceForward(callBody, 0); + if (callBody[callStart] !== "(") return false; + + const callEnd = findMatchingParen(callBody, callStart); + if (callEnd === -1 || skipWhitespaceForward(callBody, callEnd + 1) !== callBody.length) return false; + + const argument = nthCallArgumentSource(callBody, callBody, callStart, callEnd, 0); + if (!argument) return false; + if (nthCallArgumentSource(callBody, callBody, callStart, callEnd, 1)) return false; + + const argumentStart = skipWhitespaceForward(callBody, argument.index); + const argumentEnd = skipWhitespaceBackward(callBody, argument.end - 1) + 1; + // 测试清理只能是 deleteMany({ where: { id: { startsWith: runId } } });任何 OR/AND/NOT、额外 key、spread/computed 都 fail closed。 + const whereValue = soleObjectPropertyValueSpan(callBody, argumentStart, argumentEnd, "where"); + if (!whereValue) return false; + + const whereStart = skipWhitespaceForward(callBody, whereValue.start); + const whereEnd = skipWhitespaceBackward(callBody, whereValue.end - 1) + 1; + const idValue = soleObjectPropertyValueSpan(callBody, whereStart, whereEnd, "id"); + if (!idValue) return false; + + const idStart = skipWhitespaceForward(callBody, idValue.start); + const idEnd = skipWhitespaceBackward(callBody, idValue.end - 1) + 1; + const startsWithValue = soleObjectPropertyValueSpan(callBody, idStart, idEnd, "startsWith"); + if (!startsWithValue) return false; + + return isRunIdStartsWithValue(callBody, startsWithValue.start, startsWithValue.end); +} + +function hasForbiddenJobPrismaDelete(filePath, content) { + // 单行 delete 无法表达 runId 前缀清理语义,始终视为 Job 运行态删除;只有 deleteMany 可走测试清理白名单。 + return ( + prismaModelMethodCallMatches(content, ["job"], ["delete"]).length > 0 || + hasPrismaModelMethodCallExcluding(content, ["job"], ["deleteMany"], (callBody) => isAllowedJobDeleteCleanupCallBody(filePath, callBody)) + ); +} + +function hasJobStatusWrite(filePath, content) { // Job 创建路径同样会写入运行态字段,S1 必须统一收敛到 JobExecutionStore。 return ( hasPrismaModelMethodCallContaining(content, ["job"], jobPrismaWriteMethods, { test: hasForbiddenJobWriteCallBody }) || + hasForbiddenJobPrismaDelete(filePath, content) || hasRawJobStatusWrite(content) ); } @@ -1524,7 +5864,7 @@ function hasJobStatusWrite(content) { function hasForbiddenJobStatusWrite(filePath, content) { // SQL 文件走原始 SQL 扫描;源码文件继续复用 Prisma 与 raw 调用扫描。 if (filePath.endsWith(".sql")) return hasForbiddenRawJobStatusWrite(filePath, content); - return hasJobStatusWrite(content); + return hasJobStatusWrite(filePath, content); } function isAllowedJobExecutionStorePath(filePath) { @@ -1656,13 +5996,13 @@ function findStateBoundaryViolations(file, violations) { if (hasForbiddenMigrationGuardSetter(file.content)) { pushViolation(violations, file.path, "STATE_GUARD_SETTER_OUTSIDE_STATE_TRANSITION", "migration runtime guard setter"); } - if (hasGuardedPrismaWrite(file.content)) { + if (hasGuardedPrismaWrite(file.path, file.content)) { pushViolation(violations, file.path, "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION", "migration guarded model write"); } return; } - if (hasGuardedPrismaWrite(file.content)) { + if (hasGuardedPrismaWrite(file.path, file.content)) { pushViolation(violations, file.path, "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION", "guarded model write"); } if (hasRuntimeGuardSetter(file.content)) { @@ -1892,6 +6232,23 @@ FOR EACH ROW EXECUTE FUNCTION app_require_state_transition_guard(); } ); + await withFixture( + { + "apps/api/prisma/migrations/20260602010101_install_audit_guard/migration.sql": ` +CREATE FUNCTION app_reject_audit_log_mutation() +RETURNS trigger AS $$ +BEGIN + RAISE EXCEPTION 'AuditLog is append-only' USING ERRCODE = '42501'; +END; +$$ LANGUAGE plpgsql; +` + }, + async (root) => { + const violations = await findS1ScopeViolations(root); + assertNoViolations(violations, "plain AuditLog append-only function install migration"); + } + ); + const failingFixtures = [ { label: "migration set_config guard setter", @@ -1948,6 +6305,14 @@ FOR EACH ROW EXECUTE FUNCTION app_require_state_transition_guard(); 'await db["auditLog"].update({ where: { id }, data: {} });' } }, + { + label: "audit computed bracket model delete outside audit module", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-computed-bracket-model.ts": + 'await db["audit" + "Log"].delete({ where: { id } });' + } + }, { label: "audit bracket method delete outside audit module", reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", @@ -1979,6 +6344,14 @@ FOR EACH ROW EXECUTE FUNCTION app_require_state_transition_guard(); 'await db["auditLog"]?.["delete"]({ where: { id } });' } }, + { + label: "audit parenthesized delegate delete outside audit module", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-parenthesized-delete.ts": + "await (db.auditLog).delete({ where: { id } });" + } + }, { label: "audit delegate alias delete outside audit module", reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", @@ -1987,6 +6360,51 @@ FOR EACH ROW EXECUTE FUNCTION app_require_state_transition_guard(); "const logs = db.auditLog; await logs.delete({ where: { id } });" } }, + { + label: "audit destructured delegate alias delete outside audit module", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-destructured-alias-delete.ts": + "const { auditLog: logs } = db; await logs.delete({ where: { id } });" + } + }, + { + label: "audit quoted destructured delegate alias delete outside audit module", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-quoted-destructured-alias-delete.ts": + 'const { "auditLog": logs } = db; await logs.delete({ where: { id } });' + } + }, + { + label: "audit assigned delegate alias delete outside audit module", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-assigned-alias-delete.ts": ` +let logs; +logs = db.auditLog; +await logs.delete({ where: { id } }); +` + } + }, + { + label: "audit Function.prototype.call delegate delete outside audit module", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-function-call-delete.ts": + "Function.prototype.call.call(db.auditLog.delete, db.auditLog, { where: { id } });" + } + }, + { + label: "audit Function.prototype.call alias delegate delete outside audit module", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-function-call-alias-delete.ts": ` +const c = Function.prototype.call; +c.call(db.auditLog.delete, db.auditLog, { where: { id } }); +` + } + }, { label: "audit update/delete in API spec outside audit module", reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", @@ -2009,6 +6427,211 @@ FOR EACH ROW EXECUTE FUNCTION app_require_state_transition_guard(); "apps/api/src/modules/assets/state.ts": 'import { StateTransitionService } from "../state-transition/index.js";' } }, + { + label: "state-transition side-effect import outside allowed path", + reason: "STATE_TRANSITION_SERVICE_IMPORT_FORBIDDEN", + files: { + "apps/api/src/modules/assets/state-side-effect.ts": 'import "../state-transition/index.js";' + } + }, + { + label: "state-transition escaped side-effect import outside allowed path", + reason: "STATE_TRANSITION_SERVICE_IMPORT_FORBIDDEN", + files: { + "apps/api/src/modules/assets/state-side-effect-escaped.ts": 'import "../state-\\u0074ransition/index.js";' + } + }, + { + label: "state-transition escaped named import outside allowed path", + reason: "STATE_TRANSITION_SERVICE_IMPORT_FORBIDDEN", + files: { + "apps/api/src/modules/assets/state-named-escaped.ts": + 'import { StateTransitionService } from "../state-\\u0074ransition/index.js";' + } + }, + { + label: "state-transition escaped export from outside allowed path", + reason: "STATE_TRANSITION_SERVICE_IMPORT_FORBIDDEN", + files: { + "apps/api/src/modules/assets/state-export-escaped.ts": + 'export { StateTransitionService } from "../state-\\u0074ransition/index.js";' + } + }, + { + label: "state-transition dynamic import outside allowed path", + reason: "STATE_TRANSITION_SERVICE_IMPORT_FORBIDDEN", + files: { + "apps/api/src/modules/assets/state-dynamic.ts": 'await import("../state-transition/index.js");' + } + }, + { + label: "state-transition backtick dynamic import outside allowed path", + reason: "STATE_TRANSITION_SERVICE_IMPORT_FORBIDDEN", + files: { + "apps/api/src/modules/assets/state-dynamic-backtick.ts": 'await import(`../state-transition/index.js`);' + } + }, + { + label: "state-transition parenthesized literal dynamic import outside allowed path", + reason: "STATE_TRANSITION_SERVICE_IMPORT_FORBIDDEN", + files: { + "apps/api/src/modules/assets/state-dynamic-parenthesized-literal.ts": + 'await import(("../state-transition/index.js"));' + } + }, + { + label: "state-transition parenthesized backtick dynamic import outside allowed path", + reason: "STATE_TRANSITION_SERVICE_IMPORT_FORBIDDEN", + files: { + "apps/api/src/modules/assets/state-dynamic-parenthesized-backtick.ts": + "await import((`../state-transition/index.js`));" + } + }, + { + label: "state-transition concatenated direct dynamic import outside allowed path", + reason: "STATE_TRANSITION_SERVICE_IMPORT_FORBIDDEN", + files: { + "apps/api/src/modules/assets/state-dynamic-concatenated-direct.ts": + 'await import("../state-transition" + "/index.js");' + } + }, + { + label: "state-transition parenthesized concatenated direct dynamic import outside allowed path", + reason: "STATE_TRANSITION_SERVICE_IMPORT_FORBIDDEN", + files: { + "apps/api/src/modules/assets/state-dynamic-parenthesized-concatenated-direct.ts": + 'await import(("../state-transition" + "/index.js"));' + } + }, + { + label: "state-transition require outside allowed path", + reason: "STATE_TRANSITION_SERVICE_IMPORT_FORBIDDEN", + files: { + "apps/api/src/modules/assets/state-require.ts": 'require("../state-transition/index.js");' + } + }, + { + label: "state-transition parenthesized literal require outside allowed path", + reason: "STATE_TRANSITION_SERVICE_IMPORT_FORBIDDEN", + files: { + "apps/api/src/modules/assets/state-require-parenthesized-literal.ts": + 'require(("../state-transition/index.js"));' + } + }, + { + label: "state-transition concatenated direct require outside allowed path", + reason: "STATE_TRANSITION_SERVICE_IMPORT_FORBIDDEN", + files: { + "apps/api/src/modules/assets/state-require-concatenated-direct.ts": + 'require("../state-" + "transition/index.js");' + } + }, + { + label: "state-transition parenthesized concatenated direct require outside allowed path", + reason: "STATE_TRANSITION_SERVICE_IMPORT_FORBIDDEN", + files: { + "apps/api/src/modules/assets/state-require-parenthesized-concatenated-direct.ts": + 'require(("../state-" + "transition/index.js"));' + } + }, + { + label: "state-transition variable dynamic import outside allowed path", + reason: "STATE_TRANSITION_SERVICE_IMPORT_FORBIDDEN", + files: { + "apps/api/src/modules/assets/state-dynamic-variable.ts": + 'const mod = "../state-transition/index.js"; await import(mod);' + } + }, + { + label: "state-transition recursive variable dynamic import outside allowed path", + reason: "STATE_TRANSITION_SERVICE_IMPORT_FORBIDDEN", + files: { + "apps/api/src/modules/assets/state-dynamic-recursive-variable.ts": + 'const stateTransitionPath = "../state-transition/index.js"; const mod = stateTransitionPath; await import(mod);' + } + }, + { + label: "state-transition variable require outside allowed path", + reason: "STATE_TRANSITION_SERVICE_IMPORT_FORBIDDEN", + files: { + "apps/api/src/modules/assets/state-require-variable.ts": + 'const mod = "../state-transition/index.js"; require(mod);' + } + }, + { + label: "state-transition concatenated variable require outside allowed path", + reason: "STATE_TRANSITION_SERVICE_IMPORT_FORBIDDEN", + files: { + "apps/api/src/modules/assets/state-require-concatenated-variable.ts": + 'const base = "../state-transition"; const mod = base + "/index.js"; require(mod);' + } + }, + { + label: "state-transition concatenated variable dynamic import outside allowed path", + reason: "STATE_TRANSITION_SERVICE_IMPORT_FORBIDDEN", + files: { + "apps/api/src/modules/projects/state-dynamic-concatenated-variable.ts": + 'const base = "../state-"; await import(base + "transition/index.js");' + } + }, + { + label: "state-transition concatenated variable direct require outside allowed path", + reason: "STATE_TRANSITION_SERVICE_IMPORT_FORBIDDEN", + files: { + "apps/api/src/modules/projects/state-require-direct-concatenated-variable.ts": + 'const base = "../state-"; require(base + "transition/index.js");' + } + }, + { + label: "state-transition static template expression dynamic import outside allowed path", + reason: "STATE_TRANSITION_SERVICE_IMPORT_FORBIDDEN", + files: { + "apps/api/src/modules/projects/state-dynamic-static-template.ts": + 'await import(`../${"state-transition"}/index.js`);' + } + }, + { + label: "state-transition static template variable dynamic import outside allowed path", + reason: "STATE_TRANSITION_SERVICE_IMPORT_FORBIDDEN", + files: { + "apps/api/src/modules/projects/state-dynamic-static-template-variable.ts": + 'const seg = "state-transition"; await import(`../${seg}/index.js`);' + } + }, + { + label: "state-transition destructured require alias outside allowed path", + reason: "STATE_TRANSITION_SERVICE_IMPORT_FORBIDDEN", + files: { + "apps/api/src/modules/projects/state-destructured-require-alias.ts": + 'const { require: load } = module; load("../state-transition/index.js");' + } + }, + { + label: "state-transition backtick require outside allowed path", + reason: "STATE_TRANSITION_SERVICE_IMPORT_FORBIDDEN", + files: { + "apps/api/src/modules/assets/state-require-backtick.ts": 'require(`../state-transition/index.js`);' + } + }, + { + label: "state-transition aliased require outside allowed path", + reason: "STATE_TRANSITION_SERVICE_IMPORT_FORBIDDEN", + files: { + "apps/api/src/modules/assets/state-require-alias.ts": + 'const r = require; return r("../state-transition/index.js");' + } + }, + { + label: "state-transition assigned aliased require outside allowed path", + reason: "STATE_TRANSITION_SERVICE_IMPORT_FORBIDDEN", + files: { + "apps/api/src/modules/assets/state-require-assigned-alias.ts": ` +let r; +r = require; +r("../state-transition/index.js"); +` + } + }, { label: "Prisma import in worker", reason: "WORKER_DB_IMPORT_FORBIDDEN", @@ -2030,6 +6653,40 @@ FOR EACH ROW EXECUTE FUNCTION app_require_state_transition_guard(); "apps/api/src/modules/projects/job.ts": "await db.job.update({ where: { id }, data: { status: 'failed' } });" } }, + { + label: "direct Job delete outside JobExecutionStore", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-delete.ts": "await db.job.delete({ where: { id } });" + } + }, + { + label: "direct Job deleteMany outside JobExecutionStore", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-delete-many.ts": 'await db.job.deleteMany({ where: { status: "running" } });' + } + }, + { + label: "test Job delete runId cleanup outside JobExecutionStore", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-cleanup.spec.ts": ` +const runId = "scope-test"; +await db.job.delete({ where: { id: { startsWith: runId } } }); +` + } + }, + { + label: "test Job deleteMany OR cleanup outside JobExecutionStore", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-cleanup.spec.ts": ` +const runId = "scope-test"; +await db.job.deleteMany({ where: { OR: [{ id: { startsWith: runId } }, { status: "running" }] } }); +` + } + }, { label: "direct Job bracket model status write outside JobExecutionStore", reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", @@ -2070,6 +6727,14 @@ FOR EACH ROW EXECUTE FUNCTION app_require_state_transition_guard(); 'await db["job"]?.["update"]({ where: { id }, data: { status } });' } }, + { + label: "direct Job parenthesized delegate status write outside JobExecutionStore", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-parenthesized-update.ts": + 'await (db.job).update({ where: { id }, data: { status: "failed" } });' + } + }, { label: "direct Job delegate alias status write outside JobExecutionStore", reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", @@ -2078,6 +6743,32 @@ FOR EACH ROW EXECUTE FUNCTION app_require_state_transition_guard(); "const jobs = db.job; await jobs.update({ where: { id }, data: { status } });" } }, + { + label: "direct Job delegate alias delete outside JobExecutionStore", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-alias-delete.ts": "const jobs = db.job; await jobs.delete({ where: { id } });" + } + }, + { + label: "direct Job destructured delegate alias status write outside JobExecutionStore", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-destructured-alias-update.ts": + 'const { job } = db; await job.update({ where: { id }, data: { status: "failed" } });' + } + }, + { + label: "direct Job assigned delegate alias status write outside JobExecutionStore", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-assigned-alias-update.ts": ` +let jobs; +jobs = db.job; +await jobs.update({ where: { id }, data: { status: "running" } }); +` + } + }, { label: "direct Job status write with comment paren outside JobExecutionStore", reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", @@ -2162,6 +6853,38 @@ await db.job.update({ // ) `await db.$executeRawUnsafe("UPDATE \\"Job\\" SET \\"status\\" = 'failed' WHERE id = $1", id);` } }, + { + label: "raw SQL Job identity-escaped keyword status write outside JobExecutionStore", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-raw-identity-escaped-keyword-status.ts": + 'await db.$executeRawUnsafe("\\U\\P\\D\\A\\T\\E \\"Job\\" SET \\"status\\" = $1 WHERE id = $2", status, id);' + } + }, + { + label: "raw SQL Job unicode-escaped table status write outside JobExecutionStore", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-raw-unicode-table.ts": + 'await db.$executeRawUnsafe("UPDATE \\"\\u004aob\\" SET \\"status\\" = $1 WHERE id = $2", status, id);' + } + }, + { + label: "raw SQL Job unicode quoted identifier status write outside JobExecutionStore", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-raw-unicode-quoted-identifier-status.ts": + `await db.$executeRawUnsafe('UPDATE U&"J\\\\006Fb" SET U&"stat\\\\0075s" = $1 WHERE id = $2', status, id);` + } + }, + { + label: "raw SQL Job custom UESCAPE unicode quoted identifier status write outside JobExecutionStore", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-raw-unicode-quoted-custom-uescape-status.ts": + `await db.$executeRawUnsafe('UPDATE U&"J!006Fb" UESCAPE \\'!\\' SET U&"stat!0075s" UESCAPE \\'!\\' = $1 WHERE id = $2', status, id);` + } + }, { label: "optional raw SQL Job status write outside JobExecutionStore", reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", @@ -2170,6 +6893,22 @@ await db.job.update({ // ) 'await db.$executeRawUnsafe?.("UPDATE \\"Job\\" SET \\"status\\" = $1 WHERE id = $2", status, id);' } }, + { + label: "parenthesized raw SQL Job status write outside JobExecutionStore", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-raw-parenthesized-status.ts": + 'await (db.$executeRawUnsafe)("UPDATE \\"Job\\" SET \\"status\\" = $1 WHERE id = $2", status, id);' + } + }, + { + label: "parenthesized optional bracket raw SQL Job status write outside JobExecutionStore", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-raw-parenthesized-optional-bracket-status.ts": + 'await (db?.["$executeRawUnsafe"])("UPDATE \\"Job\\" SET \\"status\\" = $1 WHERE id = $2", status, id);' + } + }, { label: "bound raw SQL Job status write outside JobExecutionStore", reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", @@ -2266,6 +7005,38 @@ await db.$executeRawUnsafe( // ) 'await db.$executeRawUnsafe("MERGE INTO \\"Job\\" j USING \\"JobPatch\\" p ON j.id = p.id WHEN MATCHED THEN UPDATE SET \\"status\\" = \'failed\'");' } }, + { + label: "raw SQL Job ONLY MERGE update outside JobExecutionStore", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-raw-only-merge.ts": + 'await db.$executeRawUnsafe("MERGE INTO ONLY \\"Job\\" j USING \\"JobPatch\\" p ON j.id = p.id WHEN MATCHED THEN UPDATE SET \\"status\\" = \'failed\'");' + } + }, + { + label: "raw SQL Job ONLY block-comment MERGE update outside JobExecutionStore", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-raw-only-block-comment-merge.ts": + 'await db.$executeRawUnsafe("MERGE/*x*/INTO ONLY \\"Job\\" j USING \\"JobPatch\\" p ON j.id = p.id WHEN MATCHED THEN UPDATE SET \\"status\\" = \'failed\'");' + } + }, + { + label: "raw SQL Job nested block-comment update outside JobExecutionStore", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-raw-nested-block-comment-update.ts": + 'await db.$executeRawUnsafe("UPDATE/* outer /* inner */ still outer */\\"Job\\" SET \\"status\\" = $1 WHERE id = $2", status, id);' + } + }, + { + label: "raw SQL Job status update after line-comment-looking string literal", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-raw-status-after-string-line-comment.ts": + `await db.$executeRawUnsafe("SELECT '-- not a comment'; UPDATE \\"Job\\" SET \\"status\\" = $1 WHERE id = $2", status, id);` + } + }, { label: "raw SQL Job multi-table truncate outside JobExecutionStore", reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", @@ -2282,6 +7053,30 @@ await db.$executeRawUnsafe( // ) 'UPDATE "Job" SET "status" = \'failed\' WHERE id = \'x\';' } }, + { + label: "migration SQL Job ONLY inheritance status update outside JobExecutionStore", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/prisma/migrations/20260602000006_bad_job_only_inheritance_update/migration.sql": + 'UPDATE ONLY "Job" * SET "status" = \'failed\' WHERE id = \'x\';' + } + }, + { + label: "migration SQL Job unicode quoted identifier status update outside JobExecutionStore", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/prisma/migrations/20260602000007_bad_job_unicode_quoted_update/migration.sql": + 'UPDATE U&"J\\006Fb" SET U&"stat\\0075s" = \'failed\' WHERE id = \'x\';' + } + }, + { + label: "migration SQL Job status update after line-comment-looking string literal", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/prisma/migrations/20260602000004_bad_job_update_after_string_line_comment/migration.sql": + `SELECT '-- not a comment'; UPDATE "Job" SET "status" = 'failed' WHERE id = 'x';` + } + }, { label: "migration SQL Job MERGE update outside JobExecutionStore", reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", @@ -2290,6 +7085,14 @@ await db.$executeRawUnsafe( // ) 'MERGE INTO "Job" j USING "JobPatch" p ON j.id = p.id WHEN MATCHED THEN UPDATE SET "status" = \'failed\';' } }, + { + label: "migration SQL Job ONLY MERGE update outside JobExecutionStore", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/prisma/migrations/20260602000005_bad_job_only_merge/migration.sql": + 'MERGE INTO ONLY "Job" j USING "JobPatch" p ON j.id = p.id WHEN MATCHED THEN UPDATE SET "status" = \'failed\';' + } + }, { label: "migration SQL Job multi-table truncate outside JobExecutionStore", reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", @@ -2330,6 +7133,14 @@ await db.$executeRawUnsafe( // ) 'await db.$executeRawUnsafe("UPDATE ONLY \\"Job\\" SET \\"status\\" = $1 WHERE id = $2", status, id);' } }, + { + label: "raw SQL ONLY Job inheritance status write outside JobExecutionStore", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-raw-only-inheritance.ts": + 'await db.$executeRawUnsafe("UPDATE ONLY \\"Job\\" * SET \\"status\\" = $1 WHERE id = $2", status, id);' + } + }, { label: "Prisma.sql variable Job status write outside JobExecutionStore", reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", @@ -2355,6 +7166,18 @@ const tableName = '"Job"'; const fieldName = '"status"'; const query = "UPDATE " + tableName + " SET " + fieldName + " = $1 WHERE id = $2"; await db.$executeRawUnsafe(query, status, id); +` + } + }, + { + label: "raw SQL Job status write through unicode-escaped intermediate constants outside JobExecutionStore", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-raw-unicode-intermediate-constants.ts": ` +const tableName = '"\\u004aob"'; +const fieldName = '"status"'; +const query = "UPDATE " + tableName + " SET " + fieldName + " = $1 WHERE id = $2"; +await db.$executeRawUnsafe(query, status, id); ` } }, @@ -2367,6 +7190,39 @@ const tableName = process.env.JOB_TABLE ?? '"Job"'; const fieldName = '"status"'; const query = "UPDATE " + tableName + " SET " + fieldName + " = $1 WHERE id = $2"; await db.$executeRawUnsafe(query, status, id); +` + } + }, + { + label: "parenthesized raw SQL dynamic write outside JobExecutionStore", + reason: "DYNAMIC_RAW_SQL_WRITE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/job-raw-parenthesized-dynamic-table.ts": ` +const table = process.env.TABLE; +const query = "UPDATE " + table + " SET status = $1"; +await (db.$executeRawUnsafe)(query, status); +` + } + }, + { + label: "raw SQL Job ONLY inheritance write through dynamic table variable outside JobExecutionStore", + reason: "DYNAMIC_RAW_SQL_WRITE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/job-raw-dynamic-only-inheritance-table.ts": ` +const tableName = process.env.JOB_TABLE ?? '"Job"'; +const query = "UPDATE ONLY " + tableName + " * j SET status = $1 WHERE id = $2"; +await db.$executeRawUnsafe(query, status, id); +` + } + }, + { + label: "raw SQL Job write through identity-escaped dynamic table variable outside JobExecutionStore", + reason: "DYNAMIC_RAW_SQL_WRITE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/job-raw-identity-escaped-dynamic-table.ts": ` +const tableName = process.env.JOB_TABLE ?? '"Job"'; +const query = "\\U\\P\\D\\A\\T\\E " + tableName + " SET status = $1 WHERE id = $2"; +await db.$executeRawUnsafe(query, status, id); ` } }, @@ -2387,6 +7243,28 @@ await db.$executeRawUnsafe(query, status, id); "apps/api/src/modules/projects/job-raw-dynamic-field-call.ts": ` const query = "UPDATE Job SET " + resolveField() + " = $1 WHERE id = $2"; await db.$executeRawUnsafe(query, value, id); +` + } + }, + { + label: "raw SQL dynamic merge table outside JobExecutionStore", + reason: "DYNAMIC_RAW_SQL_WRITE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/job-raw-dynamic-merge-table.ts": ` +const tableName = process.env.JOB_TABLE ?? '"Job"'; +const query = "MERGE INTO " + tableName + " j USING \\"JobPatch\\" p ON j.id = p.id WHEN MATCHED THEN UPDATE SET status = $1"; +await db.$executeRawUnsafe(query, status); +` + } + }, + { + label: "raw SQL dynamic merge only table outside JobExecutionStore", + reason: "DYNAMIC_RAW_SQL_WRITE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/job-raw-dynamic-merge-only-table.ts": ` +const tableName = process.env.JOB_TABLE ?? '"Job"'; +const query = "MERGE INTO ONLY " + tableName + " j USING \\"JobPatch\\" p ON j.id = p.id WHEN MATCHED THEN UPDATE SET status = $1"; +await db.$executeRawUnsafe(query, status); ` } }, @@ -2422,6 +7300,14 @@ await db.$executeRaw(query); 'await db.$executeRawUnsafe("UPDATE \\"AuditLog\\" SET \\"action\\" = $1 WHERE id = $2", action, id);' } }, + { + label: "raw SQL AuditLog inheritance update outside trigger assertion", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-inheritance-update.ts": + 'await db.$executeRawUnsafe("UPDATE \\"AuditLog\\" * SET \\"action\\" = $1 WHERE id = $2", action, id);' + } + }, { label: "raw SQL AuditLog delete outside trigger assertion", reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", @@ -2430,6 +7316,102 @@ await db.$executeRaw(query); 'await db.$executeRawUnsafe("DELETE FROM \\"AuditLog\\" WHERE id = $1", id);' } }, + { + label: "raw SQL AuditLog identity-escaped keyword delete outside trigger assertion", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-identity-escaped-delete.ts": + 'await db.$executeRawUnsafe("\\D\\E\\L\\E\\T\\E FROM \\"AuditLog\\" WHERE id = $1", id);' + } + }, + { + label: "raw SQL AuditLog hex-escaped delete outside trigger assertion", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-hex-delete.ts": + 'await db.$executeRawUnsafe("DELETE FROM \\"\\x41uditLog\\" WHERE id = $1", id);' + } + }, + { + label: "raw SQL AuditLog unicode quoted identifier delete outside trigger assertion", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-unicode-quoted-identifier-delete.ts": + `await db.$executeRawUnsafe('DELETE FROM U&"Audit\\\\004Cog" WHERE id = $1', id);` + } + }, + { + label: "raw SQL AuditLog custom UESCAPE unicode quoted identifier delete outside trigger assertion", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-unicode-quoted-custom-uescape-delete.ts": + `await db.$executeRawUnsafe('DELETE FROM U&"Audit!004Cog" UESCAPE \\'!\\' WHERE id = $1', id);` + } + }, + { + label: "raw SQL AuditLog delete after line-comment-looking string literal", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-delete-after-string-line-comment.ts": + `await db.$executeRawUnsafe("SELECT '-- not a comment'; DELETE FROM \\"AuditLog\\" WHERE id = $1", id);` + } + }, + { + label: "raw SQL AuditLog delete after block-comment-looking string literal", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-delete-after-string-block-comment.ts": + `await db.$executeRawUnsafe("SELECT '/* not a comment */'; DELETE FROM \\"AuditLog\\" WHERE id = $1", id);` + } + }, + { + label: "raw SQL AuditLog upsert outside trigger assertion", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-upsert.ts": + 'await db.$executeRawUnsafe("INSERT INTO \\"AuditLog\\" (\\"id\\", \\"action\\") VALUES ($1, $2) ON CONFLICT (\\"id\\") DO UPDATE SET \\"action\\" = EXCLUDED.\\"action\\"", id, action);' + } + }, + { + label: "raw SQL AuditLog block-comment upsert outside trigger assertion", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-block-comment-upsert.ts": + 'await db.$executeRawUnsafe("INSERT/*x*/INTO \\"AuditLog\\" (\\"id\\", \\"action\\") VALUES ($1, $2) ON/*x*/CONFLICT (\\"id\\") DO/*x*/UPDATE SET \\"action\\" = EXCLUDED.\\"action\\"", id, action);' + } + }, + { + label: "raw SQL AuditLog merge outside trigger assertion", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-merge.ts": + 'await db.$executeRawUnsafe("MERGE INTO \\"AuditLog\\" a USING \\"AuditLogPatch\\" p ON a.id = p.id WHEN MATCHED THEN UPDATE SET \\"action\\" = p.\\"action\\"");' + } + }, + { + label: "raw SQL AuditLog block-comment merge outside trigger assertion", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-block-comment-merge.ts": + 'await db.$executeRawUnsafe("MERGE/*x*/INTO \\"AuditLog\\" a USING \\"AuditLogPatch\\" p ON a.id = p.id WHEN MATCHED THEN UPDATE SET \\"action\\" = p.\\"action\\"");' + } + }, + { + label: "raw SQL AuditLog block-comment delete outside trigger assertion", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-block-comment-delete.ts": + 'await db.$executeRawUnsafe("DELETE/*x*/FROM \\"AuditLog\\" WHERE id = $1", id);' + } + }, + { + label: "raw SQL AuditLog block-comment update outside trigger assertion", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-block-comment-update.ts": + 'await db.$executeRawUnsafe("UPDATE/*x*/\\"AuditLog\\" SET \\"action\\" = $1 WHERE id = $2", action, id);' + } + }, { label: "raw SQL AuditLog delete through dot whitespace executor access", reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", @@ -2462,6 +7444,56 @@ await db.$executeRaw(query); 'await db?.["$executeRawUnsafe"]("DELETE FROM \\"AuditLog\\" WHERE id = $1", id);' } }, + { + label: "raw SQL AuditLog delete through computed executor access", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-computed-delete.ts": + 'await db["$execute" + "RawUnsafe"]("DELETE FROM \\"AuditLog\\" WHERE id = $1", id);' + } + }, + { + label: "raw SQL AuditLog delete through three-part computed executor access", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-computed-three-part-delete.ts": + 'await db["$execute" + "Raw" + "Unsafe"]("DELETE FROM \\"AuditLog\\" WHERE id = $1", id);' + } + }, + { + label: "raw SQL AuditLog delete through computed executor name alias", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-computed-alias-delete.ts": ` +const rawName = "$executeRawUnsafe"; +await db[rawName]("DELETE FROM \\"AuditLog\\" WHERE id = $1", id); +` + } + }, + { + label: "raw SQL AuditLog delete through optional computed executor access", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-optional-computed-delete.ts": + 'await db?.["$execute" + "RawUnsafe"]("DELETE FROM \\"AuditLog\\" WHERE id = $1", id);' + } + }, + { + label: "raw SQL AuditLog delete through parenthesized executor access", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-parenthesized-delete.ts": + 'await (db.$executeRawUnsafe)("DELETE FROM \\"AuditLog\\" WHERE id = $1", id);' + } + }, + { + label: "raw SQL AuditLog delete through parenthesized bracket executor access", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-parenthesized-bracket-delete.ts": + 'await (db["$executeRawUnsafe"])("DELETE FROM \\"AuditLog\\" WHERE id = $1", id);' + } + }, { label: "raw SQL AuditLog delete through bracket call executor access", reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", @@ -2478,6 +7510,22 @@ await db.$executeRaw(query); 'await db?.["$executeRawUnsafe"]?.call(db, "DELETE FROM \\"AuditLog\\" WHERE id = $1", id);' } }, + { + label: "raw SQL AuditLog delete through bracket call operation executor access", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-bracket-call-operation-delete.ts": + 'await db.$executeRawUnsafe["call"](db, \'DELETE FROM "AuditLog" WHERE id = $1\', id);' + } + }, + { + label: "raw SQL AuditLog delete through computed call operation executor access", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-computed-call-operation-delete.ts": + 'await db.$executeRawUnsafe["ca" + "ll"](db, \'DELETE FROM "AuditLog" WHERE id = $1\', id);' + } + }, { label: "raw SQL AuditLog delete through bracket bind executor access", reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", @@ -2486,6 +7534,550 @@ await db.$executeRaw(query); 'await db["$executeRawUnsafe"].bind(db)("DELETE FROM \\"AuditLog\\" WHERE id = $1", id);' } }, + { + label: "raw SQL AuditLog delete through bracket bind operation executor access", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-bracket-bind-operation-delete.ts": + 'await db.$executeRawUnsafe["bind"](db)(\'DELETE FROM "AuditLog" WHERE id = $1\', id);' + } + }, + { + label: "raw SQL AuditLog delete through computed bind operation executor access", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-computed-bind-operation-delete.ts": + 'await db.$executeRawUnsafe["bi" + "nd"](db)(\'DELETE FROM "AuditLog" WHERE id = $1\', id);' + } + }, + { + label: "raw SQL AuditLog delete through apply executor access", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-apply-delete.ts": + 'await db.$executeRawUnsafe.apply(db, ["DELETE FROM \\"AuditLog\\" WHERE id = $1", id]);' + } + }, + { + label: "raw SQL Job status write through bracket apply operation executor access", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-raw-bracket-apply-operation-status.ts": + 'await db.$executeRawUnsafe["apply"](db, [\'UPDATE "Job" SET "status" = $1 WHERE id = $2\', status, id]);' + } + }, + { + label: "raw SQL AuditLog delete through Function.prototype.call executor access", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-function-prototype-call-delete.ts": + 'Function.prototype.call.call(db.$executeRawUnsafe, db, \'DELETE FROM "AuditLog" WHERE id = $1\', id);' + } + }, + { + label: "raw SQL AuditLog delete through Function.prototype.call.apply executor access", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-function-prototype-call-apply-delete.ts": + 'Function.prototype.call.apply(db.$executeRawUnsafe, [db, \'DELETE FROM "AuditLog" WHERE id = $1\', id]);' + } + }, + { + label: "raw SQL AuditLog delete through Function.prototype.call alias executor access", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-function-prototype-call-alias-delete.ts": ` +const c = Function.prototype.call; +c.call(db.$executeRawUnsafe, db, 'DELETE FROM "AuditLog" WHERE id = $1', id); +` + } + }, + { + label: "raw SQL AuditLog delete through Function.prototype.call alias apply executor access", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-function-prototype-call-alias-apply-delete.ts": ` +const c = Function.prototype.call; +c.apply(db.$executeRawUnsafe, [db, 'DELETE FROM "AuditLog" WHERE id = $1', id]); +` + } + }, + { + label: "raw SQL AuditLog delete through assigned Function.prototype.call alias executor access", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-function-prototype-call-assigned-alias-delete.ts": ` +let c; +c = Function.prototype.call; +c.call(db.$executeRawUnsafe, db, 'DELETE FROM "AuditLog" WHERE id = $1', id); +` + } + }, + { + label: "raw SQL Job status write through Reflect.apply executor access", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-raw-reflect-apply-status.ts": + 'Reflect.apply(db.$executeRawUnsafe, db, ["UPDATE \\"Job\\" SET \\"status\\" = $1 WHERE id = $2", status, id]);' + } + }, + { + label: "raw SQL AuditLog delete through Reflect.apply.call executor access", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-reflect-apply-call-delete.ts": ` +Reflect.apply.call( + Reflect, + db.$executeRawUnsafe, + db, + ["DELETE FROM \\"AuditLog\\" WHERE id = $1", id] +); +` + } + }, + { + label: "raw SQL Job status write through Reflect.apply.apply executor access", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-raw-reflect-apply-apply-status.ts": ` +Reflect.apply.apply(Reflect, [ + db.$executeRawUnsafe, + db, + ["UPDATE \\"Job\\" SET \\"status\\" = $1 WHERE id = $2", status, id] +]); +` + } + }, + { + label: "raw SQL AuditLog delete through Reflect.apply Function.prototype.call executor access", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-reflect-function-prototype-call-delete.ts": + 'Reflect.apply(Function.prototype.call, db.$executeRawUnsafe, [db, \'DELETE FROM "AuditLog" WHERE id = $1\', id]);' + } + }, + { + label: "raw SQL AuditLog delete through Reflect.apply alias Function.prototype.call executor access", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-reflect-apply-alias-function-prototype-call-delete.ts": ` +const ra = Reflect.apply; +ra(Function.prototype.call, db.$executeRawUnsafe, [db, 'DELETE FROM "AuditLog" WHERE id = $1', id]); +` + } + }, + { + label: "raw SQL Job status write through Function.prototype.apply executor access", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-raw-function-prototype-apply-status.ts": + 'Function.prototype.apply.call(db.$executeRawUnsafe, db, [\'UPDATE "Job" SET "status" = $1 WHERE id = $2\', status, id]);' + } + }, + { + label: "raw SQL Job status write through Function.prototype.apply.apply executor access", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-raw-function-prototype-apply-apply-status.ts": + 'Function.prototype.apply.apply(db.$executeRawUnsafe, [db, [\'UPDATE "Job" SET "status" = $1 WHERE id = $2\', status, id]]);' + } + }, + { + label: "raw SQL Job status write through Function.prototype.apply alias executor access", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-raw-function-prototype-apply-alias-status.ts": ` +const a = Function.prototype.apply; +a.call(db.$executeRawUnsafe, db, ['UPDATE "Job" SET "status" = $1 WHERE id = $2', status, id]); +` + } + }, + { + label: "raw SQL Job status write through Function.prototype.apply alias apply executor access", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-raw-function-prototype-apply-alias-apply-status.ts": ` +const a = Function.prototype.apply; +a.apply(db.$executeRawUnsafe, [db, ['UPDATE "Job" SET "status" = $1 WHERE id = $2', status, id]]); +` + } + }, + { + label: "raw SQL Job status write through Reflect.apply Function.prototype.apply executor access", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-raw-reflect-function-prototype-apply-status.ts": + 'Reflect.apply(Function.prototype.apply, db.$executeRawUnsafe, [db, [\'UPDATE "Job" SET "status" = $1 WHERE id = $2\', status, id]]);' + } + }, + { + label: "raw SQL Job status write through assigned Reflect.apply alias Function.prototype.apply executor access", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-raw-reflect-apply-assigned-alias-function-prototype-apply-status.ts": ` +let ra; +ra = Reflect.apply; +ra(Function.prototype.apply, db.$executeRawUnsafe, [db, ['UPDATE "Job" SET "status" = $1 WHERE id = $2', status, id]]); +` + } + }, + { + label: "raw SQL AuditLog delete through Reflect.apply alias executor access", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-reflect-apply-alias-direct-delete.ts": ` +const ra = Reflect.apply; +ra(db.$executeRawUnsafe, db, ['DELETE FROM "AuditLog" WHERE id = $1', id]); +` + } + }, + { + label: "raw SQL AuditLog delete through propagated Reflect.apply alias executor access", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-reflect-apply-propagated-alias-direct-delete.ts": ` +const r1 = Reflect.apply; +const r2 = r1; +r2(db.$executeRawUnsafe, db, ['DELETE FROM "AuditLog" WHERE id = $1', id]); +` + } + }, + { + label: "raw SQL AuditLog delete through optional Reflect.apply alias executor access", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-optional-reflect-apply-alias-direct-delete.ts": ` +const ra = Reflect.apply; +ra?.(db.$executeRawUnsafe, db, ['DELETE FROM "AuditLog" WHERE id = $1', id]); +` + } + }, + { + label: "raw SQL AuditLog delete through bound Reflect.apply alias executor access", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-bound-reflect-apply-alias-delete.ts": ` +const ra = Reflect.apply.bind(Reflect); +ra(db.$executeRawUnsafe, db, ["DELETE FROM \\"AuditLog\\" WHERE id = $1", id]); +` + } + }, + { + label: "raw SQL AuditLog delete through null-bound Reflect.apply alias executor access", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-null-bound-reflect-apply-alias-delete.ts": ` +const ra = Reflect.apply.bind(null); +ra(db.$executeRawUnsafe, db, ["DELETE FROM \\"AuditLog\\" WHERE id = $1", id]); +` + } + }, + { + label: "raw SQL Job status write through optional bound Reflect.apply alias executor access", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-raw-optional-bound-reflect-apply-alias-status.ts": ` +const ra = Reflect.apply.bind(Reflect); +ra?.(db.$executeRawUnsafe, db, ['UPDATE "Job" SET "status" = $1 WHERE id = $2', status, id]); +` + } + }, + { + label: "raw SQL AuditLog delete through bound Function.prototype.call alias executor access", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-bound-function-prototype-call-alias-delete.ts": ` +const c = Function.prototype.call.bind(Function.prototype.call); +c(db.$executeRawUnsafe, db, "DELETE FROM \\"AuditLog\\" WHERE id = $1", id); +` + } + }, + { + label: "raw SQL AuditLog delete through bracket bound Function.prototype.call alias executor access", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-bracket-bound-function-prototype-call-alias-delete.ts": ` +const c = Function.prototype["call"].bind(Function.prototype["call"]); +c(db.$executeRawUnsafe, db, 'DELETE FROM "AuditLog" WHERE id = $1', id); +` + } + }, + { + label: "raw SQL AuditLog delete through optional bracket bound Function.prototype.call alias executor access", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-optional-bracket-bound-function-prototype-call-alias-delete.ts": ` +const pc = Function.prototype["call"].bind(Function.prototype["call"]); +pc?.(db.$executeRawUnsafe, db, 'DELETE FROM "AuditLog" WHERE id = $1', id); +` + } + }, + { + label: "raw SQL AuditLog delete through computed bound Function.prototype.call alias executor access", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-computed-bound-function-prototype-call-alias-delete.ts": ` +const c = Function.prototype["ca" + "ll"].bind(Function.prototype["ca" + "ll"]); +c(db.$executeRawUnsafe, db, 'DELETE FROM "AuditLog" WHERE id = $1', id); +` + } + }, + { + label: "raw SQL Job status write through bracket bound Function.prototype.apply alias executor access", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-raw-bracket-bound-function-prototype-apply-alias-status.ts": ` +const a = Function.prototype["apply"].bind(Function.prototype["apply"]); +a(db.$executeRawUnsafe, db, ['UPDATE "Job" SET "status" = $1 WHERE id = $2', status, id]); +` + } + }, + { + label: "raw SQL AuditLog delete through Function.prototype.call bound target immediate call", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-function-prototype-call-bind-target-delete.ts": ` +Function.prototype.call.bind(db.$executeRawUnsafe)( + db, + 'DELETE FROM "AuditLog" WHERE id = $1', + id +); +` + } + }, + { + label: "raw SQL AuditLog delete through optional Function.prototype call bind target immediate call", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-function-prototype-optional-call-bind-target-delete.ts": ` +Function.prototype.call?.bind(db.$executeRawUnsafe)( + db, + 'DELETE FROM "AuditLog" WHERE id = $1', + id +); +` + } + }, + { + label: "raw SQL AuditLog delete through bracket Function.prototype call bind target immediate call", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-function-prototype-bracket-call-bind-target-delete.ts": ` +Function.prototype["call"].bind(db.$executeRawUnsafe)( + db, + 'DELETE FROM "AuditLog" WHERE id = $1', + id +); +` + } + }, + { + label: "raw SQL AuditLog delete through computed Function.prototype call bind target immediate call", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-function-prototype-computed-call-bind-target-delete.ts": ` +Function.prototype["ca" + "ll"].bind(db.$executeRawUnsafe)( + db, + 'DELETE FROM "AuditLog" WHERE id = $1', + id +); +` + } + }, + { + label: "raw SQL Job status write through Function.prototype.apply bound target immediate call", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-raw-function-prototype-apply-bind-target-status.ts": ` +Function.prototype.apply.bind(db.$executeRawUnsafe)( + db, + ['UPDATE "Job" SET "status" = $1 WHERE id = $2', status, id] +); +` + } + }, + { + label: "raw SQL Job status write through computed Function.prototype apply bind target immediate call", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-raw-function-prototype-computed-apply-bind-target-status.ts": ` +Function.prototype["ap" + "ply"].bind(db.$executeRawUnsafe)( + db, + ['UPDATE "Job" SET "status" = $1 WHERE id = $2', status, id] +); +` + } + }, + { + label: "raw SQL AuditLog delete through Function.prototype.call bound target alias", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-function-prototype-call-bind-target-alias-delete.ts": ` +const exec = Function.prototype.call.bind(db.$executeRawUnsafe); +exec(db, 'DELETE FROM "AuditLog" WHERE id = $1', id); +` + } + }, + { + label: "raw SQL AuditLog delete through optional Function.prototype.call bound target alias", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-optional-function-prototype-call-bind-target-alias-delete.ts": ` +const exec = Function.prototype.call.bind(db.$executeRawUnsafe); +exec?.(db, 'DELETE FROM "AuditLog" WHERE id = $1', id); +` + } + }, + { + label: "raw SQL AuditLog delete through assigned Function.prototype.call bound target alias", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-function-prototype-call-bind-target-assigned-alias-delete.ts": ` +let exec; +exec = Function.prototype.call.bind(db.$executeRawUnsafe); +exec(db, 'DELETE FROM "AuditLog" WHERE id = $1', id); +` + } + }, + { + label: "raw SQL Job status write through Reflect.apply computed executor access", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-raw-reflect-apply-computed-status.ts": + 'Reflect.apply((db["$execute" + "Raw" + "Unsafe"]), db, ["UPDATE \\"Job\\" SET \\"status\\" = $1 WHERE id = $2", status, id]);' + } + }, + { + label: "raw SQL AuditLog delete through Reflect bracket apply executor access", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-reflect-bracket-apply-delete.ts": ` +Reflect["apply"](db.$executeRawUnsafe, db, [ + 'DELETE FROM "AuditLog" WHERE id = $1', + id +]); +` + } + }, + { + label: "raw SQL AuditLog delete through Reflect computed apply executor access", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-reflect-computed-apply-delete.ts": ` +Reflect["ap" + "ply"](db.$executeRawUnsafe, db, [ + 'DELETE FROM "AuditLog" WHERE id = $1', + id +]); +` + } + }, + { + label: "raw SQL Job status write through Reflect.apply executor name alias", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-raw-reflect-apply-alias-status.ts": ` +const rawName = "$executeRawUnsafe"; +Reflect.apply(db[rawName], db, ["UPDATE \\"Job\\" SET \\"status\\" = $1 WHERE id = $2", status, id]); +` + } + }, + { + label: "raw SQL AuditLog delete through Reflect.apply executor name alias", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-reflect-apply-alias-delete.ts": ` +const rawName = "$executeRawUnsafe"; +Reflect.apply(db[rawName], db, ["DELETE FROM \\"AuditLog\\" WHERE id = $1", id]); +` + } + }, + { + label: "guarded raw SQL update through Reflect.apply executor name alias outside state-transition", + reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION", + files: { + "apps/api/src/modules/projects/version-raw-reflect-apply-alias-update.ts": ` +const rawName = "$executeRawUnsafe"; +Reflect.apply(db[rawName], db, ["UPDATE \\"GameVersion\\" SET \\"status\\" = $1 WHERE id = $2", status, id]); +` + } + }, + { + label: "dynamic raw SQL write through Reflect.apply executor name alias", + reason: "DYNAMIC_RAW_SQL_WRITE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/job-raw-reflect-apply-alias-dynamic.ts": ` +const rawName = "$executeRawUnsafe"; +const tableName = process.env.TABLE; +const query = "UPDATE " + tableName + " SET status = $1"; +Reflect.apply(db[rawName], db, [query, status]); +` + } + }, + { + label: "raw SQL AuditLog delete through Reflect.apply plain executor alias", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-reflect-apply-plain-alias-delete.ts": ` +const execRaw = db.$executeRawUnsafe; +Reflect.apply(execRaw, db, ['DELETE FROM "AuditLog" WHERE id = $1', id]); +` + } + }, + { + label: "raw SQL Job status write through Reflect.apply bound executor alias", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-raw-reflect-apply-bound-alias-status.ts": ` +const execRaw = db.$executeRawUnsafe.bind(db); +Reflect.apply(execRaw, db, ['UPDATE "Job" SET "status" = $1 WHERE id = $2', status, id]); +` + } + }, + { + label: "guarded raw SQL update through Reflect.apply assigned executor alias outside state-transition", + reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION", + files: { + "apps/api/src/modules/projects/version-raw-reflect-apply-assigned-alias-update.ts": ` +let execRaw; +execRaw = db.$executeRawUnsafe; +Reflect.apply(execRaw, db, ['UPDATE "GameVersion" SET "status" = $1 WHERE id = $2', status, id]); +` + } + }, + { + label: "dynamic raw SQL write through Reflect.apply destructured executor alias", + reason: "DYNAMIC_RAW_SQL_WRITE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/job-raw-reflect-apply-destructured-alias-dynamic.ts": ` +const { $executeRawUnsafe: execRaw } = db; +const tableName = process.env.TABLE; +const query = "UPDATE " + tableName + " SET status = $1"; +Reflect.apply(execRaw, db, [query, status]); +` + } + }, + { + label: "raw SQL AuditLog delete through Reflect.apply computed destructured executor alias", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-reflect-apply-computed-destructured-alias-delete.ts": ` +const rawName = "$executeRawUnsafe"; +const { [rawName]: execRaw } = db; +Reflect.apply(execRaw, db, ['DELETE FROM "AuditLog" WHERE id = $1', id]); +` + } + }, + { + label: "raw SQL AuditLog delete through assigned executor name alias", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-assigned-computed-key-delete.ts": ` +let rawName; +rawName = "$executeRawUnsafe"; +await db[rawName]('DELETE FROM "AuditLog" WHERE id = $1', id); +` + } + }, { label: "raw SQL AuditLog delete through optional bracket bind executor access", reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", @@ -2501,6 +8093,244 @@ await db.$executeRaw(query); "apps/api/src/modules/projects/audit-raw-bracket-bound-alias-delete.ts": ` const execRaw = db["$executeRawUnsafe"].bind(db); await execRaw("DELETE FROM \\"AuditLog\\" WHERE id = $1", id); +` + } + }, + { + label: "raw SQL AuditLog delete through plain alias direct call", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-plain-alias-direct-delete.ts": ` +const execRaw = db.$executeRawUnsafe; +await execRaw("DELETE FROM \\"AuditLog\\" WHERE id = $1", id); +` + } + }, + { + label: "raw SQL AuditLog delete through mixed ternary bound executor alias", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-mixed-ternary-bound-alias-delete.ts": ` +const execRaw = ok ? db.$executeRawUnsafe.bind(db) : safe; +await execRaw("DELETE FROM \\"AuditLog\\" WHERE id = $1", id); +` + } + }, + { + label: "raw SQL AuditLog delete through propagated plain alias direct call", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-propagated-plain-alias-direct-delete.ts": ` +const execRaw = db.$executeRawUnsafe; +const runRaw = execRaw; +await runRaw('DELETE FROM "AuditLog" WHERE id = $1', id); +` + } + }, + { + label: "raw SQL AuditLog delete through parenthesized plain alias direct call", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-parenthesized-plain-alias-direct-delete.ts": ` +const execRaw = (db.$executeRawUnsafe); +await execRaw("DELETE FROM \\"AuditLog\\" WHERE id = $1", id); +` + } + }, + { + label: "raw SQL AuditLog delete through destructured executor alias direct call", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-destructured-alias-direct-delete.ts": ` +const { $executeRawUnsafe } = db; +await $executeRawUnsafe("DELETE FROM \\"AuditLog\\" WHERE id = $1", id); +` + } + }, + { + label: "raw SQL AuditLog delete through renamed destructured executor alias direct call", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-renamed-destructured-alias-direct-delete.ts": ` +const { $executeRawUnsafe: execRaw } = db; +await execRaw("DELETE FROM \\"AuditLog\\" WHERE id = $1", id); +` + } + }, + { + label: "raw SQL AuditLog delete through quoted destructured executor alias direct call", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-quoted-destructured-alias-direct-delete.ts": ` +const { "$executeRawUnsafe": execRaw } = db; +await execRaw("UPDATE \\"AuditLog\\" SET action = $1 WHERE id = $2", action, id); +` + } + }, + { + label: "raw SQL AuditLog delete through computed destructured executor alias direct call", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-computed-destructured-alias-direct-delete.ts": ` +const { ["$executeRawUnsafe"]: execRaw } = db; +await execRaw("UPDATE \\"AuditLog\\" SET action = $1 WHERE id = $2", action, id); +` + } + }, + { + label: "raw SQL AuditLog delete through post-declaration destructured executor alias", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-assigned-destructured-alias-delete.ts": ` +let execRaw; +({ $executeRawUnsafe: execRaw } = db); +await execRaw("DELETE FROM \\"AuditLog\\" WHERE id = $1", id); +` + } + }, + { + label: "raw SQL AuditLog delete through assigned computed destructured executor alias", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-assigned-computed-destructured-alias-delete.ts": ` +let execRaw; +({ ["$executeRawUnsafe"]: execRaw } = db); +await execRaw("UPDATE \\"AuditLog\\" SET action = $1 WHERE id = $2", action, id); +` + } + }, + { + label: "raw SQL Job status write through assigned executor alias direct call", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-raw-assigned-alias-direct-status.ts": ` +let execRaw; +execRaw = db.$executeRawUnsafe; +await execRaw("UPDATE \\"Job\\" SET \\"status\\" = $1 WHERE id = $2", status, id); +` + } + }, + { + label: "raw SQL Job status write through assigned logical plain executor alias", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-raw-assigned-logical-plain-alias-status.ts": ` +let runRaw; +runRaw = maybe || db.$executeRawUnsafe; +await runRaw("UPDATE \\"Job\\" SET \\"status\\" = $1 WHERE id = $2", status, id); +` + } + }, + { + label: "raw SQL Job status write through post-declaration destructured executor alias", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-raw-assigned-destructured-alias-status.ts": ` +let $executeRawUnsafe; +({ $executeRawUnsafe } = db); +await $executeRawUnsafe("UPDATE \\"Job\\" SET \\"status\\" = $1 WHERE id = $2", status, id); +` + } + }, + { + label: "raw SQL Job status write through assigned bound executor alias direct call", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-raw-assigned-bound-alias-direct-status.ts": ` +let execRaw; +execRaw = db.$executeRawUnsafe.bind(db); +await execRaw("UPDATE \\"Job\\" SET \\"status\\" = $1 WHERE id = $2", status, id); +` + } + }, + { + label: "raw SQL Job status write through propagated bound alias direct call", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-raw-propagated-bound-alias-direct-status.ts": ` +const execRaw = db.$executeRawUnsafe.bind(db); +const runRaw = execRaw; +await runRaw('UPDATE "Job" SET "status" = $1 WHERE id = $2', status, id); +` + } + }, + { + label: "optional raw SQL Job status write through plain alias direct call", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-raw-plain-alias-optional-direct-status.ts": ` +const execRaw = db.$executeRawUnsafe; +await execRaw?.("UPDATE \\"Job\\" SET \\"status\\" = $1 WHERE id = $2", status, id); +` + } + }, + { + label: "raw SQL AuditLog delete through plain alias call", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-plain-alias-call-delete.ts": ` +const execRaw = db.$executeRawUnsafe; +await execRaw.call(db, "DELETE FROM \\"AuditLog\\" WHERE id = $1", id); +` + } + }, + { + label: "raw SQL Job status write through plain alias call", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-raw-plain-alias-call-status.ts": ` +const execRaw = db.$executeRawUnsafe; +await execRaw.call(db, "UPDATE \\"Job\\" SET \\"status\\" = $1 WHERE id = $2", status, id); +` + } + }, + { + label: "optional raw SQL AuditLog delete through plain alias call", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-plain-alias-optional-call-delete.ts": ` +const execRaw = db.$executeRawUnsafe; +await execRaw?.call(db, "DELETE FROM \\"AuditLog\\" WHERE id = $1", id); +` + } + }, + { + label: "raw SQL AuditLog delete through plain alias bind", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-plain-alias-bind-delete.ts": ` +const execRaw = db.$executeRawUnsafe; +await execRaw.bind(db)("DELETE FROM \\"AuditLog\\" WHERE id = $1", id); +` + } + }, + { + label: "raw SQL Job status write through plain alias bind", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-raw-plain-alias-bind-status.ts": ` +const execRaw = db.$executeRawUnsafe; +await execRaw.bind(db)("UPDATE \\"Job\\" SET \\"status\\" = $1 WHERE id = $2", status, id); +` + } + }, + { + label: "optional raw SQL Job status write through plain alias bind", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-raw-plain-alias-optional-bind-status.ts": ` +const execRaw = db.$executeRawUnsafe; +await execRaw?.bind(db)("UPDATE \\"Job\\" SET \\"status\\" = $1 WHERE id = $2", status, id); +` + } + }, + { + label: "raw SQL AuditLog delete through optional bracket plain alias call", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-optional-bracket-plain-alias-call-delete.ts": ` +const execRaw = db?.["$executeRawUnsafe"]; +await execRaw.call(db, "DELETE FROM \\"AuditLog\\" WHERE id = $1", id); ` } }, @@ -2521,6 +8351,219 @@ await execRaw("DELETE FROM \\"AuditLog\\" WHERE id = $1", id) "apps/api/src/modules/projects/audit-raw-optional-bracket-bound-alias-no-semicolon-delete.ts": ` const execRaw = db?.["$executeRawUnsafe"]?.bind(db) await execRaw("DELETE FROM \\"AuditLog\\" WHERE id = $1", id) +` + } + }, + { + label: "raw SQL AuditLog delete through object property wrapped executor", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-object-property-wrapper-delete.ts": ` +const ops = { execRaw: db.$executeRawUnsafe }; +await ops.execRaw.call(db, 'DELETE FROM "AuditLog" WHERE id = $1', id); +` + } + }, + { + label: "raw SQL AuditLog delete through object wrapper destructured executor", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-object-wrapper-destructured-delete.ts": ` +const ops = { execRaw: db.$executeRawUnsafe }; +const { execRaw } = ops; +await execRaw("DELETE FROM \\"AuditLog\\" WHERE id = $1", id); +` + } + }, + { + label: "raw SQL AuditLog delete through object wrapper property alias", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-object-wrapper-property-alias-delete.ts": ` +const ops = { execRaw: db.$executeRawUnsafe }; +const runRaw = ops.execRaw; +await runRaw("DELETE FROM \\"AuditLog\\" WHERE id = $1", id); +` + } + }, + { + label: "raw SQL AuditLog delete through object shorthand wrapper executor", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-object-shorthand-wrapper-delete.ts": ` +const execRaw = db.$executeRawUnsafe.bind(db); +const ops = { execRaw }; +await ops.execRaw('DELETE FROM "AuditLog" WHERE id=1'); +` + } + }, + { + label: "raw SQL AuditLog delete through Object.assign shorthand wrapper executor", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-object-assign-shorthand-wrapper-delete.ts": ` +const execRaw = db.$executeRawUnsafe.bind(db); +const box = Object.assign({}, { execRaw }); +await box.execRaw('DELETE FROM "AuditLog" WHERE id = $1', id); +` + } + }, + { + label: "raw SQL AuditLog delete through array wrapper executor", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-array-wrapper-delete.ts": ` +const ops = [db.$executeRawUnsafe]; +await ops[0]("DELETE FROM \\"AuditLog\\" WHERE id = $1", id); +` + } + }, + { + label: "raw SQL AuditLog delete through array wrapper skipped destructured executor", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-array-wrapper-skipped-destructured-delete.ts": ` +const ops = [1, db.$executeRawUnsafe.bind(db)]; +const [, execRaw] = ops; +await execRaw('DELETE FROM "AuditLog" WHERE id=1'); +` + } + }, + { + label: "raw SQL AuditLog delete through array wrapper assigned skipped destructured executor", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-array-wrapper-assigned-skipped-destructured-delete.ts": ` +const ops = [1, db.$executeRawUnsafe.bind(db)]; +let execRaw; +([, execRaw] = ops); +await execRaw('DELETE FROM "AuditLog" WHERE id=1'); +` + } + }, + { + label: "raw SQL AuditLog delete through function return wrapper executor", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-function-return-wrapper-delete.ts": ` +function raw() { return db.$executeRawUnsafe; } +await raw()("DELETE FROM \\"AuditLog\\" WHERE id = $1", id); +` + } + }, + { + label: "raw SQL AuditLog delete through function returned object wrapper executor", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-function-return-object-wrapper-delete.ts": ` +async function f(db) { + function getBox() { + return { raw: db.$executeRawUnsafe.bind(db) }; + } + const { raw } = getBox(); + await raw('DELETE FROM "AuditLog" WHERE id=1'); +} +` + } + }, + { + label: "raw SQL AuditLog delete through parenthesized function returned object wrapper executor", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-parenthesized-function-return-object-wrapper-delete.ts": ` +function rawBox() { return ({ execRaw: db.$executeRawUnsafe }); } +rawBox().execRaw('DELETE FROM "AuditLog" WHERE id = $1', id); +` + } + }, + { + label: "raw SQL AuditLog delete through function returned array wrapper executor", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-function-return-array-wrapper-delete.ts": ` +function getBox() { return [db.$executeRawUnsafe.bind(db)]; } +await getBox()[0]('DELETE FROM "AuditLog" WHERE id=1'); +` + } + }, + { + label: "raw SQL guarded state truncate through parenthesized function returned array wrapper executor", + reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION", + files: { + "apps/api/src/modules/projects/review-raw-parenthesized-function-return-array-wrapper-truncate.ts": ` +function rawArray() { return ([db.$executeRawUnsafe]); } +rawArray()[0]('TRUNCATE TABLE "ReviewRecord"'); +` + } + }, + { + label: "raw SQL Job status write through parenthesized arrow returned object wrapper executor", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-raw-parenthesized-arrow-return-object-wrapper-status.ts": ` +const rawArrowBox = () => ({ execRaw: db.$executeRawUnsafe }); +rawArrowBox().execRaw('UPDATE "Job" SET "status" = $1 WHERE id = $2', status, id); +` + } + }, + { + label: "raw SQL AuditLog delete through typed arrow return wrapper executor", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-typed-arrow-return-wrapper-delete.ts": ` +const raw = (): typeof db.$executeRawUnsafe => db.$executeRawUnsafe; +raw()('DELETE FROM "AuditLog" WHERE id = $1', id); +` + } + }, + { + label: "raw SQL Job status write through typed arrow returned object wrapper executor", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-raw-typed-arrow-return-object-wrapper-status.ts": ` +const box = (): { execRaw: typeof db.$executeRawUnsafe } => ({ execRaw: db.$executeRawUnsafe }); +box().execRaw('UPDATE "Job" SET "status" = $1 WHERE id = $2', status, id); +` + } + }, + { + label: "raw SQL AuditLog delete through nested object wrapper destructured executor", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-nested-object-wrapper-destructured-delete.ts": ` +const raw = { inner: { exec: db.$executeRawUnsafe } }; +const { inner: { exec } } = raw; +await exec('DELETE FROM "AuditLog" WHERE id = $1', id); +` + } + }, + { + label: "raw SQL AuditLog delete through arrow return wrapper executor", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-arrow-return-wrapper-delete.ts": ` +const raw = () => db.$executeRawUnsafe; +await raw()('DELETE FROM "AuditLog" WHERE id = $1', id); +` + } + }, + { + label: "raw SQL AuditLog delete through block arrow return wrapper executor", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-block-arrow-return-wrapper-delete.ts": ` +const getRaw = () => { return db.$executeRawUnsafe.bind(db); }; +await getRaw()('DELETE FROM "AuditLog" WHERE id = $1', id); +` + } + }, + { + label: "raw SQL AuditLog delete through function expression return wrapper executor", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-raw-function-expression-return-wrapper-delete.ts": ` +const raw = function () { return db.$executeRawUnsafe; }; +await raw()('DELETE FROM "AuditLog" WHERE id = $1', id); ` } }, @@ -2622,6 +8665,14 @@ await execRaw("DELETE FROM \\"AuditLog\\" WHERE id = $1", id); 'await db.$executeRawUnsafe("DROP FUNCTION app_reject_audit_log_mutation() CASCADE");' } }, + { + label: "raw SQL AuditLog unicode-codepoint drop append-only function outside trigger assertion", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-drop-function-unicode-codepoint.ts": + 'await db.$executeRawUnsafe("DROP FUNCTION \\u{61}pp_reject_audit_log_mutation() CASCADE");' + } + }, { label: "raw SQL AuditLog replace append-only function outside trigger assertion", reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", @@ -2638,6 +8689,421 @@ await execRaw("DELETE FROM \\"AuditLog\\" WHERE id = $1", id); 'await db.$executeRawUnsafe("ALTER FUNCTION app_reject_audit_log_mutation() OWNER TO app");' } }, + { + label: "raw SQL AuditLog alter IF EXISTS append-only function outside trigger assertion", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-alter-function-if-exists.ts": + 'await db.$executeRawUnsafe("ALTER FUNCTION IF EXISTS app_reject_audit_log_mutation() OWNER TO postgres");' + } + }, + { + label: "raw SQL AuditLog multi-function drop append-only function outside trigger assertion", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-drop-function-list.ts": + 'await db.$executeRawUnsafe("DROP FUNCTION IF EXISTS harmless(), app_reject_audit_log_mutation() CASCADE");' + } + }, + { + label: "raw SQL AuditLog drop append-only routine outside trigger assertion", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-drop-routine.ts": + 'await db.$executeRawUnsafe("DROP ROUTINE IF EXISTS app_reject_audit_log_mutation() CASCADE");' + } + }, + { + label: "raw SQL AuditLog alter append-only routine outside trigger assertion", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-alter-routine.ts": + 'await db.$executeRawUnsafe("ALTER ROUTINE app_reject_audit_log_mutation() OWNER TO app");' + } + }, + { + label: "raw SQL AuditLog alter IF EXISTS append-only routine outside trigger assertion", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-alter-routine-if-exists.ts": + 'await db.$executeRawUnsafe("ALTER ROUTINE IF EXISTS app_reject_audit_log_mutation() OWNER TO postgres");' + } + }, + { + label: "raw SQL dynamic drop function outside trigger assertion", + reason: "DYNAMIC_RAW_SQL_WRITE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-drop-dynamic-function.ts": ` +const fn = process.env.AUDIT_FN ?? 'app_reject_audit_log_mutation()'; +const query = "DROP FUNCTION " + fn + " CASCADE"; +await db.$executeRawUnsafe(query); +` + } + }, + { + label: "raw SQL dynamic alter function outside trigger assertion", + reason: "DYNAMIC_RAW_SQL_WRITE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-alter-dynamic-function.ts": ` +const fn = process.env.AUDIT_FN ?? 'app_reject_audit_log_mutation()'; +const query = "ALTER FUNCTION " + fn + " OWNER TO app"; +await db.$executeRawUnsafe(query); +` + } + }, + { + label: "raw SQL dynamic drop routine outside trigger assertion", + reason: "DYNAMIC_RAW_SQL_WRITE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-drop-dynamic-routine.ts": ` +const fn = process.env.AUDIT_FN ?? 'app_reject_audit_log_mutation()'; +const query = "DROP ROUTINE " + fn + " CASCADE"; +await db.$executeRawUnsafe(query); +` + } + }, + { + label: "raw SQL dynamic alter routine outside trigger assertion", + reason: "DYNAMIC_RAW_SQL_WRITE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-alter-dynamic-routine.ts": ` +const fn = process.env.AUDIT_FN ?? 'app_reject_audit_log_mutation()'; +const query = "ALTER ROUTINE " + fn + " OWNER TO app"; +await db.$executeRawUnsafe(query); +` + } + }, + { + label: "raw SQL dynamic drop function list outside trigger assertion", + reason: "DYNAMIC_RAW_SQL_WRITE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-drop-dynamic-function-list.ts": ` +const fn = process.env.AUDIT_FN ?? 'app_reject_audit_log_mutation()'; +const query = "DROP FUNCTION IF EXISTS harmless(), " + fn + " CASCADE"; +await db.$executeRawUnsafe(query); +` + } + }, + { + label: "raw SQL dynamic create function outside trigger assertion", + reason: "DYNAMIC_RAW_SQL_WRITE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-create-dynamic-function.ts": ` +const fn = process.env.AUDIT_FN ?? 'app_reject_audit_log_mutation()'; +const query = "CREATE FUNCTION " + fn + " RETURNS trigger AS $$ BEGIN RETURN OLD; END; $$ LANGUAGE plpgsql"; +await db.$executeRawUnsafe(query); +` + } + }, + { + label: "raw SQL dynamic create or replace function outside trigger assertion", + reason: "DYNAMIC_RAW_SQL_WRITE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-replace-dynamic-function.ts": ` +const fn = process.env.AUDIT_FN ?? 'app_reject_audit_log_mutation()'; +const query = "CREATE OR REPLACE FUNCTION " + fn + " RETURNS trigger AS $$ BEGIN RETURN OLD; END; $$ LANGUAGE plpgsql"; +await db.$executeRawUnsafe(query); +` + } + }, + { + label: "migration replace AuditLog append-only function", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/prisma/migrations/20260602010102_replace_audit_guard/migration.sql": ` +CREATE OR REPLACE FUNCTION app_reject_audit_log_mutation() +RETURNS trigger AS $$ +BEGIN + RAISE EXCEPTION 'AuditLog is append-only' USING ERRCODE = '42501'; +END; +$$ LANGUAGE plpgsql; +` + } + }, + { + label: "migration block-comment tampered AuditLog append-only function with RETURN NEW", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/prisma/migrations/20260602010108_block_comment_tampered_audit_guard_return/migration.sql": ` +CREATE/*x*/FUNCTION app_reject_audit_log_mutation() +RETURNS trigger AS $$ +BEGIN + RETURN NEW; + RAISE EXCEPTION 'AuditLog is append-only' USING ERRCODE = '42501'; +END; +$$ LANGUAGE plpgsql; +` + } + }, + { + label: "migration tampered AuditLog append-only function with RETURN NEW", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/prisma/migrations/20260602010105_tampered_audit_guard_return/migration.sql": ` +CREATE FUNCTION app_reject_audit_log_mutation() +RETURNS trigger AS $$ +BEGIN + RETURN NEW; + RAISE EXCEPTION 'AuditLog is append-only' USING ERRCODE = '42501'; +END; +$$ LANGUAGE plpgsql; +` + } + }, + { + label: "migration tampered AuditLog append-only function with IF branch", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/prisma/migrations/20260602010106_tampered_audit_guard_if/migration.sql": ` +CREATE FUNCTION app_reject_audit_log_mutation() +RETURNS trigger AS $$ +BEGIN + IF false THEN + RAISE EXCEPTION 'AuditLog is append-only' USING ERRCODE = '42501'; + END IF; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; +` + } + }, + { + label: "migration mixed canonical and bad AuditLog append-only function definitions", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/prisma/migrations/20260602010107_mixed_audit_guard/migration.sql": ` +CREATE FUNCTION app_reject_audit_log_mutation() +RETURNS trigger AS $$ +BEGIN + RAISE EXCEPTION 'AuditLog is append-only' USING ERRCODE = '42501'; +END; +$$ LANGUAGE plpgsql; + +CREATE FUNCTION app_reject_audit_log_mutation() +RETURNS trigger AS $$ +BEGIN + RETURN NEW; +END; +$$ LANGUAGE plpgsql; +` + } + }, + { + label: "migration drop AuditLog append-only function", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/prisma/migrations/20260602010103_drop_audit_guard/migration.sql": + "DROP FUNCTION app_reject_audit_log_mutation() CASCADE;" + } + }, + { + label: "migration drop AuditLog append-only function in list", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/prisma/migrations/20260602010120_drop_audit_guard_function_list/migration.sql": + "DROP FUNCTION IF EXISTS harmless(), app_reject_audit_log_mutation() CASCADE;" + } + }, + { + label: "migration drop AuditLog append-only routine", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/prisma/migrations/20260602010121_drop_audit_guard_routine/migration.sql": + "DROP ROUTINE IF EXISTS app_reject_audit_log_mutation() CASCADE;" + } + }, + { + label: "migration block-comment drop AuditLog append-only function", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/prisma/migrations/20260602010109_block_comment_drop_audit_guard/migration.sql": + "DROP/*x*/FUNCTION app_reject_audit_log_mutation() CASCADE;" + } + }, + { + label: "migration nested block-comment drop AuditLog append-only function", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/prisma/migrations/20260602010116_nested_block_comment_drop_audit_guard/migration.sql": + "DROP/* outer /* inner */ still outer */FUNCTION app_reject_audit_log_mutation() CASCADE;" + } + }, + { + label: "migration alter AuditLog append-only function", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/prisma/migrations/20260602010104_alter_audit_guard/migration.sql": + "ALTER FUNCTION app_reject_audit_log_mutation() OWNER TO app;" + } + }, + { + label: "migration alter IF EXISTS AuditLog append-only function", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/prisma/migrations/20260602010124_alter_if_exists_audit_guard/migration.sql": + "ALTER FUNCTION IF EXISTS app_reject_audit_log_mutation() OWNER TO postgres;" + } + }, + { + label: "migration alter AuditLog append-only routine", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/prisma/migrations/20260602010122_alter_audit_guard_routine/migration.sql": + "ALTER ROUTINE app_reject_audit_log_mutation() OWNER TO app;" + } + }, + { + label: "migration alter IF EXISTS AuditLog append-only routine", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/prisma/migrations/20260602010125_alter_if_exists_audit_guard_routine/migration.sql": + "ALTER ROUTINE IF EXISTS app_reject_audit_log_mutation() OWNER TO postgres;" + } + }, + { + label: "migration block-comment alter AuditLog append-only function", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/prisma/migrations/20260602010110_block_comment_alter_audit_guard/migration.sql": + "ALTER/*x*/FUNCTION app_reject_audit_log_mutation() OWNER TO app;" + } + }, + { + label: "migration SQL AuditLog block-comment delete", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/prisma/migrations/20260602010111_block_comment_audit_delete/migration.sql": + 'DELETE/*x*/FROM "AuditLog" WHERE id = \'x\';' + } + }, + { + label: "migration SQL AuditLog unicode quoted identifier delete", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/prisma/migrations/20260602010119_unicode_quoted_audit_delete/migration.sql": + 'DELETE FROM U&"Audit\\004Cog" WHERE id = \'x\';' + } + }, + { + label: "migration SQL AuditLog custom UESCAPE unicode quoted identifier delete", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/prisma/migrations/20260602010123_unicode_quoted_custom_uescape_audit_delete/migration.sql": + 'DELETE FROM U&"Audit!004Cog" UESCAPE \'!\' WHERE id = \'x\';' + } + }, + { + label: "migration SQL AuditLog inheritance update", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/prisma/migrations/20260602010118_audit_inheritance_update/migration.sql": + 'UPDATE "AuditLog" * SET "action" = \'changed\' WHERE id = \'x\';' + } + }, + { + label: "migration SQL AuditLog nested block-comment delete", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/prisma/migrations/20260602010117_nested_block_comment_audit_delete/migration.sql": + 'DELETE/* outer /* inner */ still outer */FROM "AuditLog" WHERE id = \'x\';' + } + }, + { + label: "migration SQL AuditLog delete after line-comment-looking string literal", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/prisma/migrations/20260602010115_audit_delete_after_string_line_comment/migration.sql": + `SELECT '-- not a comment'; DELETE FROM "AuditLog" WHERE id = 'x';` + } + }, + { + label: "migration SQL AuditLog upsert", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/prisma/migrations/20260602010112_audit_upsert/migration.sql": + 'INSERT INTO "AuditLog" ("id", "action") VALUES (\'x\', \'created\') ON CONFLICT ("id") DO UPDATE SET "action" = EXCLUDED."action";' + } + }, + { + label: "migration SQL AuditLog merge", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/prisma/migrations/20260602010113_audit_merge/migration.sql": + 'MERGE INTO "AuditLog" a USING "AuditLogPatch" p ON a.id = p.id WHEN MATCHED THEN UPDATE SET "action" = p."action";' + } + }, + { + label: "migration SQL AuditLog block-comment merge", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/prisma/migrations/20260602010114_block_comment_audit_merge/migration.sql": + 'MERGE/*x*/INTO "AuditLog" a USING "AuditLogPatch" p ON a.id = p.id WHEN MATCHED THEN UPDATE SET "action" = p."action";' + } + }, + { + label: "migration SQL Job block-comment merge outside JobExecutionStore", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/prisma/migrations/20260602010112_block_comment_job_merge/migration.sql": + 'MERGE/*x*/INTO "Job" j USING "JobPatch" p ON j.id = p.id WHEN MATCHED THEN UPDATE SET "status" = \'failed\';' + } + }, + { + label: "raw SQL AuditLog delete through direct generic executor", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-direct-generic-raw-delete.ts": + 'await db.$executeRawUnsafe("DELETE FROM \\"AuditLog\\" WHERE id = $1", id);' + } + }, + { + label: "raw SQL Job update through direct generic executor", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-direct-generic-raw-update.ts": + 'await db.$executeRaw("UPDATE \\"Job\\" SET \\"status\\" = $1 WHERE id = $2", status, id);' + } + }, + { + label: "raw SQL Job update through bound alias generic executor", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-bound-alias-generic-raw-update.ts": ` +const execRaw = db.$executeRawUnsafe.bind(db); +await execRaw("UPDATE \\"Job\\" SET \\"status\\" = $1 WHERE id = $2", status, id); +` + } + }, + { + label: "raw SQL AuditLog delete through plain alias generic executor", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-plain-alias-generic-raw-delete.ts": ` +const execRaw = db.$executeRawUnsafe; +await execRaw("DELETE FROM \\"AuditLog\\" WHERE id = $1", id); +` + } + }, + { + label: "raw SQL AuditLog delete through optional plain alias generic executor", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-optional-plain-alias-generic-raw-delete.ts": ` +const execRaw = db.$executeRawUnsafe; +await execRaw?.("DELETE FROM \\"AuditLog\\" WHERE id = $1", id); +` + } + }, + { + label: "raw SQL AuditLog delete through wrapped generic executor", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-wrapped-generic-raw-delete.ts": ` +const box = { execRaw: db.$executeRawUnsafe.bind(db) }; +await box.execRaw("DELETE FROM \\"AuditLog\\" WHERE id = $1", id); +` + } + }, { label: "raw SQL AuditLog trigger disable through intermediate table constant", reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", @@ -2724,6 +9190,17 @@ await db.$executeRawUnsafe(query); const tableName = '"AuditLog"'; const query = Prisma.sql\`DELETE FROM \${Prisma.raw(tableName)} WHERE id = \${id}\`; await db.$executeRaw(query); +` + } + }, + { + label: "Prisma.sql AuditLog delete through unicode-escaped Prisma.raw intermediate table constant", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-prisma-raw-unicode-intermediate.ts": ` +const tableName = '"\\u0041uditLog"'; +const query = Prisma.sql\`DELETE FROM \${Prisma.raw(tableName)} WHERE id = \${id}\`; +await db.$executeRaw(query); ` } }, @@ -2762,6 +9239,756 @@ export class JobApiService { await db.job.update({ where: { id: "bad" }, data: { status: "failed" } }); } } +` + } + }, + { + label: "computed Job status write outside JobExecutionStore", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-computed-bracket-model.ts": + 'await db["j" + "o" + "b"].update({ where: { id }, data: { status: "failed" } });' + } + }, + { + label: "parenthesized computed Job status write outside JobExecutionStore", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-parenthesized-computed-bracket-model.ts": + 'await db[("j" + "o" + "b")].update({ where: { id }, data: { status: "failed" } });' + } + }, + { + label: "computed Job update method write outside JobExecutionStore", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-computed-bracket-method.ts": + 'await db.job[("up" + "date")]({ where: { id }, data: { status: "failed" } });' + } + }, + { + label: "Function.prototype.call Job status write outside JobExecutionStore", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-function-prototype-call-status.ts": + "Function.prototype.call.call(db.job.update, db.job, { where: { id }, data: { status } });" + } + }, + { + label: "Function.prototype.call propagated alias Job status write outside JobExecutionStore", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-function-prototype-call-propagated-alias-status.ts": ` +const c1 = Function.prototype.call; +const c2 = c1; +c2.call(db.job.update, db.job, { where: { id }, data: { status: "failed" } }); +` + } + }, + { + label: "bracket call operation audit delegate delete outside audit module", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-bracket-call-operation-delete.ts": + 'await db.auditLog.delete["call"](db.auditLog, { where: { id } });' + } + }, + { + label: "bracket call operation guarded Prisma write outside state-transition", + reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION", + files: { + "apps/api/src/modules/projects/version-bracket-call-operation-status.ts": + 'await db["gameVersion"].update["call"](db.gameVersion, { where: { id }, data: { status: "x" } });' + } + }, + { + label: "bracket apply operation Job status write outside JobExecutionStore", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-bracket-apply-operation-status.ts": + 'await db.job.update["apply"](db.job, [{ where: { id }, data: { status: "failed" } }]);' + } + }, + { + label: "computed apply operation Job status write outside JobExecutionStore", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-computed-apply-operation-status.ts": + 'await db.job.update["app" + "ly"](db.job, [{ where: { id }, data: { status: "failed" } }]);' + } + }, + { + label: "bracket bind operation Job status write outside JobExecutionStore", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-bracket-bind-operation-status.ts": + 'await db.job.update["bind"](db.job)({ where: { id }, data: { status: "failed" } });' + } + }, + { + label: "delegate propagated alias Job status write outside JobExecutionStore", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-delegate-propagated-alias-status.ts": ` +const jobs = db.job; +const queueJobs = jobs; +await queueJobs.update({ where: { id }, data: { status: "failed" } }); +` + } + }, + { + label: "delegate mixed ternary alias Job delete outside JobExecutionStore", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-delegate-mixed-ternary-alias-delete.ts": ` +const jobs = ok ? db.job : safeJobs; +await jobs.delete({ where: { id } }); +` + } + }, + { + label: "delegate assigned logical alias audit delete outside audit module", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-delegate-assigned-logical-alias-delete.ts": ` +let logs; +logs = maybe && db.auditLog; +await logs.delete({ where: { id } }); +` + } + }, + { + label: "delegate mixed ternary alias guarded write outside state-transition", + reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION", + files: { + "apps/api/src/modules/projects/version-delegate-mixed-ternary-alias-status.ts": ` +const versions = ok ? db.gameVersion : safeVersions; +await versions.update({ where: { id }, data: { status: "active" } }); +` + } + }, + { + label: "delegate object wrapper destructured Job status write outside JobExecutionStore", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-delegate-object-wrapper-destructured-status.ts": ` +const ops = { jobs: db.job }; +const { jobs } = ops; +await jobs.update({ where: { id }, data: { status: "failed" } }); +` + } + }, + { + label: "delegate nested object wrapper destructured guarded write outside state-transition", + reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION", + files: { + "apps/api/src/modules/projects/version-delegate-nested-object-wrapper-destructured-status.ts": ` +const box = { inner: { version: db.gameVersion } }; +const { inner: { version } } = box; +await version.update({ where: { id }, data: { status: "active" } }); +` + } + }, + { + label: "delegate array wrapper Job status write outside JobExecutionStore", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-delegate-array-wrapper-status.ts": ` +const ops = [db.job]; +await ops[0].update({ where: { id }, data: { status: "failed" } }); +` + } + }, + { + label: "delegate function return wrapper audit delete outside audit module", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-delegate-function-return-wrapper-delete.ts": ` +function audit() { return db.auditLog; } +await audit().delete({ where: { id } }); +` + } + }, + { + label: "delegate function returned object wrapper audit delete outside audit module", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-delegate-function-return-object-wrapper-delete.ts": ` +async function f(db) { + function getBox() { + return { logs: db.auditLog }; + } + const { logs } = getBox(); + await logs.delete({ where: { id: "1" } }); +} +` + } + }, + { + label: "delegate arrow return wrapper guarded write outside state-transition", + reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION", + files: { + "apps/api/src/modules/projects/version-delegate-arrow-return-wrapper-status.ts": ` +const getVersion = () => db.gameVersion; +await getVersion().update({ where: { id }, data: { status: "active" } }); +` + } + }, + { + label: "delegate block arrow return wrapper guarded write outside state-transition", + reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION", + files: { + "apps/api/src/modules/projects/version-delegate-block-arrow-return-wrapper-status.ts": ` +const getVersion = () => { return db.gameVersion; }; +await getVersion().update({ where: { id }, data: { status: "active" } }); +` + } + }, + { + label: "delegate function returned object wrapper guarded write outside state-transition", + reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION", + files: { + "apps/api/src/modules/projects/version-delegate-function-return-object-wrapper-status.ts": ` +async function f(db) { + function getBox() { + return { versions: db.gameVersion }; + } + const { versions } = getBox(); + await versions.update({ where: { id: "1" }, data: { status: "x" } }); +} +` + } + }, + { + label: "delegate parenthesized function returned object wrapper guarded write outside state-transition", + reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION", + files: { + "apps/api/src/modules/projects/version-delegate-parenthesized-function-return-object-wrapper-status.ts": ` +function delegateBox() { return ({ versions: db.gameVersion }); } +delegateBox().versions.update({ where: { id }, data: { status: 'active' } }); +` + } + }, + { + label: "delegate function expression return wrapper audit delete outside audit module", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-delegate-function-expression-return-wrapper-delete.ts": ` +const audit = function () { return db.auditLog; }; +await audit().delete({ where: { id } }); +` + } + }, + { + label: "method object wrapper Job status write outside JobExecutionStore", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-method-object-wrapper-status.ts": ` +const ops = { updateJob: db.job.update }; +await ops.updateJob.call(db.job, { where: { id }, data: { status: "failed" } }); +` + } + }, + { + label: "method bound alias Job delete outside JobExecutionStore", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-method-bound-alias-delete.ts": ` +const deleteJob = db.job.delete.bind(db.job); +await deleteJob({ where: { id } }); +` + } + }, + { + label: "method plain alias Job delete outside JobExecutionStore", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-method-plain-alias-delete.ts": ` +const del = db.job.delete; +await del({ where: { id } }); +` + } + }, + { + label: "method assigned plain alias Job deleteMany outside JobExecutionStore", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-method-assigned-plain-alias-delete-many.ts": ` +let delMany; +delMany = db.job.deleteMany; +await delMany({ where: { status: "running" } }); +` + } + }, + { + label: "method mixed ternary plain alias Job delete outside JobExecutionStore", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-method-mixed-ternary-plain-alias-delete.ts": ` +const del = ok ? db.job.delete : safe; +await del.call(db.job, { where: { id } }); +` + } + }, + { + label: "method mixed ternary bound alias Job delete outside JobExecutionStore", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-method-mixed-ternary-bound-alias-delete.ts": ` +const del = ok ? db.job.delete.bind(db.job) : safe; +await del({ where: { id } }); +` + } + }, + { + label: "method assigned bound alias Job delete outside JobExecutionStore", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-method-assigned-bound-alias-delete.ts": ` +let del; +del = db.job.delete.bind(db.job); +await del({ where: { id } }); +` + } + }, + { + label: "method mixed assigned ternary bound alias Job delete outside JobExecutionStore", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-method-mixed-assigned-ternary-bound-alias-delete.ts": ` +let del; +del = ok ? db.job.delete.bind(db.job) : safe; +await del({ where: { id } }); +` + } + }, + { + label: "method destructured alias Job delete outside JobExecutionStore", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-method-destructured-alias-delete.ts": ` +const { delete: deleteJob } = db.job; +await deleteJob.call(db.job, { where: { id } }); +` + } + }, + { + label: "method Object.assign wrapper Job delete outside JobExecutionStore", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-method-object-assign-wrapper-delete.ts": ` +const deleteJob = db.job.delete.bind(db.job); +const box = Object.assign({}, { deleteJob }); +await box.deleteJob({ where: { id } }); +` + } + }, + { + label: "method function return wrapper Job deleteMany outside JobExecutionStore", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-method-function-return-wrapper-delete-many.ts": ` +const getDeleteMany = () => { return db.job.deleteMany.bind(db.job); }; +await getDeleteMany()({ where: { status: "running" } }); +` + } + }, + { + label: "method direct bound alias audit delegate delete outside audit module", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-method-direct-bound-alias-delete.ts": ` +const del = db.auditLog.delete.bind(db.auditLog); +await del({ where: { id } }); +` + } + }, + { + label: "method plain alias audit delegate delete outside audit module", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-method-plain-alias-delete.ts": ` +const deleteAudit = db.auditLog.delete; +await deleteAudit({ where: { id } }); +` + } + }, + { + label: "method assigned mixed ternary plain alias audit delegate delete outside audit module", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-method-assigned-mixed-ternary-plain-alias-delete.ts": ` +let del; +del = ok ? db.auditLog.delete : safe; +await del({ where: { id } }); +` + } + }, + { + label: "method assigned bound alias audit delegate delete outside audit module", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-method-assigned-bound-alias-delete.ts": ` +let del; +del = db.auditLog.delete.bind(db.auditLog); +await del({ where: { id } }); +` + } + }, + { + label: "method mixed ternary bound alias audit delegate delete outside audit module", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-method-mixed-ternary-bound-alias-delete.ts": ` +const del = ok ? db.auditLog.delete.bind(db.auditLog) : safe; +await del({ where: { id } }); +` + } + }, + { + label: "method destructured bound alias audit delegate delete outside audit module", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-method-destructured-bound-alias-delete.ts": ` +const { delete: del0 } = db.auditLog; +const del = del0.bind(db.auditLog); +await del({ where: { id } }); +` + } + }, + { + label: "method direct bound alias guarded write outside state-transition", + reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION", + files: { + "apps/api/src/modules/projects/version-method-direct-bound-alias-status.ts": ` +const updateVersion = db.gameVersion.update.bind(db.gameVersion); +await updateVersion({ where: { id }, data: { status: "active" } }); +` + } + }, + { + label: "method assigned plain alias guarded write outside state-transition", + reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION", + files: { + "apps/api/src/modules/projects/version-method-assigned-plain-alias-status.ts": ` +let updateVersion; +updateVersion = db.gameVersion.update; +await updateVersion({ where: { id }, data: { status: "active" } }); +` + } + }, + { + label: "method plain alias call guarded write outside state-transition", + reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION", + files: { + "apps/api/src/modules/projects/version-method-plain-alias-call-status.ts": ` +const updateVersion = db.gameVersion.update; +await updateVersion.call(db.gameVersion, { where: { id }, data: { status: "active" } }); +` + } + }, + { + label: "method logical plain alias guarded write outside state-transition", + reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION", + files: { + "apps/api/src/modules/projects/version-method-logical-plain-alias-call-status.ts": ` +const update = maybe || db.gameVersion.update; +await update.call(db.gameVersion, { where: { id }, data: { status: "active" } }); +` + } + }, + { + label: "method assigned bound alias guarded write outside state-transition", + reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION", + files: { + "apps/api/src/modules/projects/version-method-assigned-bound-alias-status.ts": ` +let update; +update = db.gameVersion.update.bind(db.gameVersion); +await update({ where: { id }, data: { status: "active" } }); +` + } + }, + { + label: "method mixed ternary bound alias guarded write outside state-transition", + reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION", + files: { + "apps/api/src/modules/projects/version-method-mixed-ternary-bound-alias-status.ts": ` +const update = ok ? db.gameVersion.update.bind(db.gameVersion) : safe; +await update({ where: { id }, data: { status: "active" } }); +` + } + }, + { + label: "method ternary bound alias guarded write outside state-transition", + reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION", + files: { + "apps/api/src/modules/projects/version-method-ternary-bound-alias-status.ts": ` +const update = ok ? db.gameVersion.update.bind(db.gameVersion) : db.gameVersion.update.bind(db.gameVersion); +await update({ where: { id }, data: { status: 'active' } }); +` + } + }, + { + label: "method logical OR bound alias audit delegate delete outside audit module", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-method-logical-or-bound-alias-delete.ts": ` +const del = maybe || db.auditLog.delete.bind(db.auditLog); +await del({ where: { id } }); +` + } + }, + { + label: "method logical AND bound alias audit delegate delete outside audit module", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-method-logical-and-bound-alias-delete.ts": ` +const del = ok && db.auditLog.delete.bind(db.auditLog); +await del({ where: { id } }); +` + } + }, + { + label: "method Object.assign wrapper guarded write outside state-transition", + reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION", + files: { + "apps/api/src/modules/projects/version-method-object-assign-wrapper-status.ts": ` +const update = db.gameVersion.update.bind(db.gameVersion); +const box = Object.assign({ safe: 1 }, { update }); +await box.update({ where: { id }, data: { status: "active" } }); +` + } + }, + { + label: "method block arrow return wrapper guarded write outside state-transition", + reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION", + files: { + "apps/api/src/modules/projects/version-method-block-arrow-return-wrapper-status.ts": ` +const getUpdate = () => { return db.gameVersion.update; }; +await getUpdate().call(db.gameVersion, { where: { id }, data: { status: "active" } }); +` + } + }, + { + label: "method object wrapper destructured guarded write outside state-transition", + reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION", + files: { + "apps/api/src/modules/projects/version-method-object-wrapper-destructured-status.ts": ` +const ops = { update: db.gameVersion.update }; +const { update } = ops; +await update({ where: { id }, data: { status: "active" } }); +` + } + }, + { + label: "method object wrapper assigned destructured guarded write outside state-transition", + reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION", + files: { + "apps/api/src/modules/projects/version-method-object-wrapper-assigned-destructured-status.ts": ` +const ops = { update: db.gameVersion.update }; +let update; +({ update } = ops); +await update.call(db.gameVersion, { where: { id }, data: { status: "active" } }); +` + } + }, + { + label: "method object wrapper property alias guarded call outside state-transition", + reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION", + files: { + "apps/api/src/modules/projects/version-method-object-wrapper-property-alias-call-status.ts": ` +const ops = { update: db.gameVersion.update }; +const fn = ops.update; +await fn.call(db.gameVersion, { where: { id }, data: { status: "active" } }); +` + } + }, + { + label: "method object wrapper property alias guarded apply outside state-transition", + reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION", + files: { + "apps/api/src/modules/projects/version-method-object-wrapper-property-alias-apply-status.ts": ` +const ops = { update: db.gameVersion.update }; +const fn = ops.update; +await fn.apply(db.gameVersion, [{ where: { id }, data: { status: "active" } }]); +` + } + }, + { + label: "method object wrapper property alias guarded bind outside state-transition", + reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION", + files: { + "apps/api/src/modules/projects/version-method-object-wrapper-property-alias-bind-status.ts": ` +const ops = { update: db.gameVersion.update }; +const fn = ops.update; +await fn.bind(db.gameVersion)({ where: { id }, data: { status: "active" } }); +` + } + }, + { + label: "method function returned object wrapper property alias guarded call outside state-transition", + reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION", + files: { + "apps/api/src/modules/projects/version-method-function-return-object-wrapper-property-alias-call-status.ts": ` +function getBox() { return { update: db.gameVersion.update }; } +const update = getBox().update; +await update.call(db.gameVersion, { where: { id }, data: { status: "x" } }); +` + } + }, + { + label: "method parenthesized function returned object wrapper guarded write outside state-transition", + reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION", + files: { + "apps/api/src/modules/projects/version-method-parenthesized-function-return-object-wrapper-status.ts": ` +function methodBox() { return ({ update: db.gameVersion.update }); } +methodBox().update({ where: { id }, data: { status: 'active' } }); +` + } + }, + { + label: "Function.prototype.call.apply Job status write outside JobExecutionStore", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-function-prototype-call-apply-status.ts": + "Function.prototype.call.apply(db.job.update, [db.job, { where: { id }, data: { status: 'failed' } }]);" + } + }, + { + label: "Function.prototype.call.apply audit delegate delete outside audit module", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-function-prototype-call-apply-delete.ts": + "Function.prototype.call.apply(db.auditLog.delete, [db.auditLog, { where: { id } }]);" + } + }, + { + label: "Function.prototype.call bound target audit delegate delete outside audit module", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-function-prototype-call-bind-target-delete.ts": ` +Function.prototype.call.bind(db.auditLog.delete)( + db.auditLog, + { where: { id } } +); +` + } + }, + { + label: "Function.prototype.apply bound target Job status write outside JobExecutionStore", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-function-prototype-apply-bind-target-status.ts": ` +Function.prototype.apply.bind(db.job.update)( + db.job, + [{ where: { id }, data: { status: "failed" } }] +); +` + } + }, + { + label: "Function.prototype computed apply bound target Job status write outside JobExecutionStore", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-function-prototype-computed-apply-bind-target-status.ts": ` +Function.prototype["ap" + "ply"].bind(db.job.update)( + db.job, + [{ where: { id }, data: { status: "failed" } }] +); +` + } + }, + { + label: "bracket bound Function.prototype.call alias Job status write outside JobExecutionStore", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-bracket-bound-function-prototype-call-alias-status.ts": ` +const pc = Function.prototype["call"].bind(Function.prototype["call"]); +pc(db.job.update, db.job, { where: { id }, data: { status: "failed" } }); +` + } + }, + { + label: "optional bracket bound Function.prototype.call alias Job status write outside JobExecutionStore", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-optional-bracket-bound-function-prototype-call-alias-status.ts": ` +const pc = Function.prototype["call"].bind(Function.prototype["call"]); +pc?.(db.job.update, db.job, { where: { id }, data: { status: "failed" } }); +` + } + }, + { + label: "Reflect bracket apply guarded Prisma write outside state-transition", + reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION", + files: { + "apps/api/src/modules/projects/version-reflect-bracket-apply-prisma-update.ts": ` +Reflect["apply"](db.gameVersion.update, db.gameVersion, [ + { where: { id }, data: { status: "active" } } +]); +` + } + }, + { + label: "null-bound Reflect.apply Job delete outside JobExecutionStore", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-null-bound-reflect-apply-delete.ts": ` +const apply = Reflect.apply.bind(null); +apply(db.job.delete, db.job, [{ where: { id } }]); +` + } + }, + { + label: "null-bound Reflect.apply audit delegate delete outside audit module", + reason: "AUDIT_UPDATE_DELETE_FORBIDDEN", + files: { + "apps/api/src/modules/projects/audit-null-bound-reflect-apply-delete.ts": ` +const apply = Reflect.apply.bind(null); +apply(db.auditLog.delete, db.auditLog, [{ where: { id } }]); +` + } + }, + { + label: "null-bound Reflect.apply guarded Prisma write outside state-transition", + reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION", + files: { + "apps/api/src/modules/projects/version-null-bound-reflect-apply-prisma-update.ts": ` +const apply = Reflect.apply.bind(null); +apply(db.gameVersion.update, db.gameVersion, [{ where: { id }, data: { status: "active" } }]); +` + } + }, + { + label: "Reflect.apply.call Function.prototype.call Job status write outside JobExecutionStore", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-reflect-apply-call-function-prototype-call-status.ts": ` +Reflect.apply.call(Reflect, Function.prototype.call, db.job.update, [ + db.job, + { where: { id }, data: { status } } +]); +` + } + }, + { + label: "computed destructured Job delegate alias write outside JobExecutionStore", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-computed-destructured-alias.ts": ` +const { ["j" + "o" + "b"]: jobs } = db; +await jobs.update({ where: { id }, data: { status: "failed" } }); +` + } + }, + { + label: "assigned variable computed Job delegate write outside JobExecutionStore", + reason: "JOB_STATUS_WRITE_OUTSIDE_EXECUTION_STORE", + files: { + "apps/api/src/modules/projects/job-assigned-computed-model.ts": ` +let modelName; +modelName = "job"; +await db[modelName].update({ where: { id }, data: { status: "failed" } }); ` } }, @@ -2780,6 +10007,14 @@ export class JobApiService { 'await db.gameVersion?.update({ where: { id }, data: { status: "active" } });' } }, + { + label: "guarded parenthesized Prisma write outside state-transition", + reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION", + files: { + "apps/api/src/modules/projects/version-parenthesized.ts": + 'await (db.gameVersion).update({ where: { id }, data: { status: "active" } });' + } + }, { label: "guarded delegate alias Prisma write outside state-transition", reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION", @@ -2788,6 +10023,39 @@ export class JobApiService { 'const versions = db.gameVersion; await versions.update({ where: { id }, data: { status: "active" } });' } }, + { + label: "guarded assigned delegate alias Prisma write outside state-transition", + reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION", + files: { + "apps/api/src/modules/projects/version-assigned-alias.ts": ` +let versions; +versions = db.gameVersion; +await versions.update({ where: { id }, data: { status: "active" } }); +` + } + }, + { + label: "guarded assigned destructured delegate alias Prisma write outside state-transition", + reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION", + files: { + "apps/api/src/modules/projects/version-assigned-destructured-alias.ts": ` +let versions; +({ gameVersion: versions } = db); +await versions.update({ where: { id }, data: { status: "active" } }); +` + } + }, + { + label: "guarded assigned computed destructured delegate alias Prisma write outside state-transition", + reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION", + files: { + "apps/api/src/modules/projects/version-assigned-computed-destructured-alias.ts": ` +let versions; +({ ["game" + "Version"]: versions } = db); +await versions.update({ where: { id }, data: { status: "active" } }); +` + } + }, { label: "guarded Prisma bracket model write outside state-transition", reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION", @@ -2796,6 +10064,14 @@ export class JobApiService { 'await db["gameVersion"].update({ where: { id }, data: { status: "active" } });' } }, + { + label: "guarded Prisma computed bracket model write outside state-transition", + reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION", + files: { + "apps/api/src/modules/projects/version-computed-bracket-model.ts": + 'await db["game" + "Version"].update({ where: { id }, data: { status: "active" } });' + } + }, { label: "guarded Prisma bracket method write outside state-transition", reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION", @@ -2812,6 +10088,90 @@ export class JobApiService { 'await db["lifecycleEvent"]["delete"]({ where: { eventId } });' } }, + { + label: "guarded raw SQL simple update outside state-transition", + reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION", + files: { + "apps/api/src/modules/projects/version-raw-update.ts": + 'await db.$executeRawUnsafe("UPDATE \\"GameVersion\\" SET \\"status\\" = $1 WHERE id = $2", status, id);' + } + }, + { + label: "guarded parenthesized raw SQL update outside state-transition", + reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION", + files: { + "apps/api/src/modules/projects/version-raw-parenthesized-update.ts": + 'await (db.$executeRawUnsafe)("UPDATE \\"GameVersion\\" SET \\"status\\" = $1 WHERE id = $2", status, id);' + } + }, + { + label: "guarded raw SQL block-comment update through optional bracket outside state-transition", + reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION", + files: { + "apps/api/src/modules/projects/version-raw-block-comment-update.ts": + 'await db?.["$executeRawUnsafe"]?.("UPDATE/*x*/\\"GameVersion\\" SET \\"status\\" = $1 WHERE id = $2", status, id);' + } + }, + { + label: "guarded raw SQL UESCAPE update through call outside state-transition", + reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION", + files: { + "apps/api/src/modules/projects/version-raw-uescape-update.ts": + 'await db.$executeRawUnsafe.call(db, "UPDATE U&\\"Game!0056ersion\\" UESCAPE \'!\' SET \\"status\\" = $1 WHERE id = $2", status, id);' + } + }, + { + label: "guarded raw SQL inheritance update through bound alias outside state-transition", + reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION", + files: { + "apps/api/src/modules/projects/version-raw-bound-alias-update.ts": ` +const execRaw = db.$executeRawUnsafe.bind(db); +await execRaw('UPDATE ONLY "GameVersion" * gv SET "status" = $1 WHERE gv.id = $2', status, id); +` + } + }, + { + label: "guarded raw SQL insert through plain alias outside state-transition", + reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION", + files: { + "apps/api/src/modules/projects/review-raw-plain-alias-insert.ts": ` +const execRaw = db.$executeRawUnsafe; +await execRaw('INSERT INTO "ReviewRecord" ("id") VALUES ($1)', id); +` + } + }, + { + label: "guarded raw SQL delete through assigned plain alias outside state-transition", + reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION", + files: { + "apps/api/src/modules/projects/lifecycle-raw-assigned-plain-alias-delete.ts": ` +let execRaw; +execRaw = db.$executeRawUnsafe; +await execRaw('DELETE FROM "LifecycleEvent" WHERE "eventId" = $1', eventId); +` + } + }, + { + label: "guarded raw SQL merge through destructured alias outside state-transition", + reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION", + files: { + "apps/api/src/modules/projects/version-raw-destructured-alias-merge.ts": ` +const { $executeRawUnsafe: execRaw } = db; +await execRaw('MERGE INTO "GameVersion" gv USING "GameVersionPatch" p ON gv.id = p.id WHEN MATCHED THEN UPDATE SET "status" = p."status"'); +` + } + }, + { + label: "guarded raw SQL truncate through assigned bound alias outside state-transition", + reason: "GUARDED_PRISMA_WRITE_OUTSIDE_STATE_TRANSITION", + files: { + "apps/api/src/modules/projects/review-raw-assigned-bound-alias-truncate.ts": ` +let execRaw; +execRaw = db.$executeRawUnsafe.bind(db); +await execRaw('TRUNCATE TABLE "ReviewRecord"'); +` + } + }, { label: "later-phase creation-agent messages path", reason: "LATER_PHASE_SCOPE_LEAK", @@ -2854,6 +10214,27 @@ export class JobApiService { "apps/api/src/modules/events/batch.ts": "export const harmless = true;" } }, + { + label: "later-phase PromptRecord app implementation content", + reason: "LATER_PHASE_SCOPE_LEAK", + files: { + "apps/api/src/modules/projects/prompt-record.ts": "export interface PromptRecord { rawPrompt: string; }" + } + }, + { + label: "later-phase DesignSection app implementation content", + reason: "LATER_PHASE_SCOPE_LEAK", + files: { + "apps/api/src/modules/projects/design-section.ts": "export interface DesignSection { projectId: string; payloadJson: unknown; }" + } + }, + { + label: "later-phase DesignSectionPatch app implementation content", + reason: "LATER_PHASE_SCOPE_LEAK", + files: { + "apps/api/src/modules/projects/design-section-patch.ts": "export interface DesignSectionPatch { id: string; patch: unknown; }" + } + }, { label: "later-phase AIGameDesignDraft filename path", reason: "LATER_PHASE_SCOPE_LEAK", @@ -2913,6 +10294,34 @@ await db.job.update({ where: { id }, data: { status: "failed" } }); } ); + await withFixture( + { + "apps/api/src/modules/state-transition/index.ts": 'import "./index.js"; export class StateTransitionService {}', + "apps/api/src/modules/state-transition/self.ts": + 'await import(`../state-transition/index.js`); require(`../state-transition/index.js`); const r = require; r(`../state-transition/index.js`); let rr; rr = require; rr("../state-transition/index.js");', + "apps/api/src/modules/projects/index.ts": + 'import "../state-transition/index.js"; await import("../state-transition/index.js"); require("../state-transition/index.js"); await import(`../state-transition/index.js`); require(`../state-transition/index.js`); await import("../state-transition" + "/index.js"); require("../state-" + "transition/index.js"); const mod = "../state-transition/index.js"; await import(mod); require(mod); const stateTransitionPath = "../state-transition/index.js"; const recursiveMod = stateTransitionPath; await import(recursiveMod); const base = "../state-transition"; const concatenatedMod = base + "/index.js"; require(concatenatedMod);' + }, + async (root) => { + const violations = await findS1ScopeViolations(root); + assertNoViolations(violations, "allowed state-transition import paths"); + } + ); + + await withFixture( + { + "apps/api/src/modules/state-transition/raw-guarded.ts": ` +const execRaw = db.$executeRawUnsafe.bind(db); +await execRaw('UPDATE "GameVersion" SET "status" = $1 WHERE id = $2', status, id); +await db.$executeRawUnsafe('TRUNCATE TABLE "ReviewRecord"'); +` + }, + async (root) => { + const violations = await findS1ScopeViolations(root); + assertNoViolations(violations, "allowed state-transition raw guarded write path"); + } + ); + await withFixture( { "apps/api/src/modules/audit/audit.spec.ts": ` @@ -2943,6 +10352,182 @@ await db.$queryRawUnsafe(query, id); } ); + await withFixture( + { + "apps/api/src/modules/projects/nonraw-wrapper.ts": [ + "const nonRaw = (sql) => sql;", + "const f1 = nonRaw;", + "const f2 = f1;", + "f2('DELETE FROM \"AuditLog\" WHERE id = $1');", + "", + "const ops = { execRaw: nonRaw };", + "const { execRaw } = ops;", + 'execRaw("DELETE FROM \\"AuditLog\\" WHERE id = $1");', + "", + "const ops2 = { execRaw: nonRaw };", + "ops2.execRaw.call(null, 'DELETE FROM \"AuditLog\" WHERE id = $1');", + "", + "const shorthand = nonRaw;", + "const opsShorthand = { shorthand };", + "opsShorthand.shorthand('DELETE FROM \"AuditLog\" WHERE id = $1');", + "const assignedBox = Object.assign({}, { shorthand });", + "assignedBox.shorthand('DELETE FROM \"AuditLog\" WHERE id = $1');", + "", + "const arr = [nonRaw];", + "arr[0]('UPDATE \"Job\" SET \"status\" = $1');", + "const arr2 = [1, nonRaw];", + "const [, arrExec] = arr2;", + "arrExec('DELETE FROM \"AuditLog\" WHERE id = $1');", + "let assignedArrExec;", + "([, assignedArrExec] = arr2);", + "assignedArrExec('DELETE FROM \"AuditLog\" WHERE id = $1');", + "", + "function raw() { return nonRaw; }", + "raw()('DELETE FROM \"AuditLog\" WHERE id = $1');", + "", + "function getBox() { return { raw: nonRaw, nested: { exec: nonRaw }, arr: [nonRaw] }; }", + "const { raw: boxedRaw, nested: { exec: nestedBoxedRaw } } = getBox();", + "boxedRaw('DELETE FROM \"AuditLog\" WHERE id = $1');", + "nestedBoxedRaw('DELETE FROM \"AuditLog\" WHERE id = $1');", + "const boxedAlias = getBox().raw;", + "boxedAlias('DELETE FROM \"AuditLog\" WHERE id = $1');", + "getBox().arr[0]('UPDATE \"Job\" SET \"status\" = $1');", + "", + "function getParenBox() { return ({ raw: nonRaw, arr: [nonRaw] }); }", + "getParenBox().raw('DELETE FROM \"AuditLog\" WHERE id = $1');", + "getParenBox().arr[0]('UPDATE \"Job\" SET \"status\" = $1');", + "", + "const nested = { inner: { exec: nonRaw } };", + "const { inner: { exec } } = nested;", + "exec('DELETE FROM \"AuditLog\" WHERE id = $1');", + "", + "const rawArrow = () => nonRaw;", + "rawArrow()('DELETE FROM \"AuditLog\" WHERE id = $1');", + "const blockArrow = () => { return nonRaw; };", + "blockArrow()('DELETE FROM \"AuditLog\" WHERE id = $1');", + "const logicalNonRaw = maybe || nonRaw;", + "logicalNonRaw('DELETE FROM \"AuditLog\" WHERE id = $1');", + "const ternaryNonRaw = ok ? nonRaw : nonRaw;", + "ternaryNonRaw('UPDATE \"Job\" SET \"status\" = $1');", + "const conditionalNonRaw = ok ? nonRaw : safe;", + "conditionalNonRaw('DELETE FROM \"AuditLog\" WHERE id = $1');", + "const typedRaw = (): typeof nonRaw => nonRaw;", + "typedRaw()('DELETE FROM \"AuditLog\" WHERE id = $1');", + "const typedBox = (): { raw: typeof nonRaw } => ({ raw: nonRaw });", + "typedBox().raw('UPDATE \"Job\" SET \"status\" = $1');", + "", + "const rawExpression = function () { return nonRaw; };", + "rawExpression()('DELETE FROM \"AuditLog\" WHERE id = $1');", + "", + "const ordinary = { inner: { update: nonRaw } };", + "const { inner: { update } } = ordinary;", + 'update({ where: { id }, data: { status: "active" } });', + "const deleteJob = (args) => args;", + "await deleteJob({ where: { id } });", + "const deleteBox = Object.assign({}, { deleteJob });", + "await deleteBox.deleteJob({ where: { id } });", + "const getDeleteMany = () => { return deleteJob; };", + 'await getDeleteMany()({ where: { status: "running" } });', + "const safeJobs = { delete: (args) => args };", + "const otherJobs = { delete: (args) => args };", + "const jobs = ok ? safeJobs : otherJobs;", + "await jobs.delete({ where: { id } });", + "const safeDelete = (args) => args;", + "const otherSafeDelete = (args) => args;", + "const safeDel = ok ? safeDelete : otherSafeDelete;", + "await safeDel.call(db.job, { where: { id } });", + "await nonRaw('DELETE FROM \"AuditLog\" WHERE id = $1', id);", + 'await import(`../${"safe-module"}/index.js`);', + 'const seg = "safe-module";', + "await import(`../${seg}/index.js`);" + ].join("\n") + }, + async (root) => { + const violations = await findS1ScopeViolations(root); + assertNoViolations(violations, "non raw wrapper aliases"); + } + ); + + await withFixture( + { + "apps/api/src/modules/projects/job-cleanup.spec.ts": ` +const runId = "scope-test"; +await prisma.job.deleteMany({ where: { id: { startsWith: runId } } }); +await db.job.deleteMany({ where: { id: { startsWith: runId } } }); +` + }, + async (root) => { + const violations = await findS1ScopeViolations(root); + assertNoViolations(violations, "test-only Job runId cleanup"); + } + ); + + await withFixture( + { + "apps/api/src/modules/projects/dynamic-select-unicode-quoted.ts": ` +const tableName = process.env.READ_TABLE ?? 'U&"J\\\\006Fb"'; +const query = "SELECT * FROM " + tableName + " WHERE id = $1"; +await db.$queryRawUnsafe(query, id); +` + }, + async (root) => { + const violations = await findS1ScopeViolations(root); + assertNoViolations(violations, "dynamic raw SQL SELECT with unicode quoted identifier"); + } + ); + + await withFixture( + { + "apps/api/src/modules/projects/non-raw-reflect-apply.ts": ` +const nonRaw = (sql) => sql; +Reflect.apply(nonRaw, null, ['DELETE FROM "AuditLog" WHERE id = $1']); +Function.prototype.call.call(nonRaw, null, 'DELETE FROM "AuditLog" WHERE id = $1'); +const c = Function.prototype.call; +const ra = Reflect.apply; +c.call(nonRaw, null, 'DELETE FROM "AuditLog" WHERE id = $1', id); +ra(nonRaw, null, ['UPDATE "Job" SET "status" = $1 WHERE id = $2', status, id]); +Reflect.apply.call(Reflect, nonRaw, null, ["DELETE FROM \\"AuditLog\\" WHERE id = $1", id]); +const boundReflectApply = Reflect.apply.bind(Reflect); +boundReflectApply(nonRaw, null, ["UPDATE \\"Job\\" SET \\"status\\" = $1 WHERE id = $2", status, id]); +const nullBoundReflectApply = Reflect.apply.bind(null); +nullBoundReflectApply(nonRaw, null, ["UPDATE \\"Job\\" SET \\"status\\" = $1 WHERE id = $2", status, id]); +const boundCall = Function.prototype.call.bind(Function.prototype.call); +boundCall(nonRaw, null, "DELETE FROM \\"AuditLog\\" WHERE id = $1", id); +Function.prototype.call.apply(nonRaw, [null, 'DELETE FROM "AuditLog" WHERE id = $1', id]); +Function.prototype.apply.apply(nonRaw, [null, ['UPDATE "Job" SET "status" = $1 WHERE id = $2', status, id]]); +Function.prototype.call.bind(nonRaw)(null, 'DELETE FROM "AuditLog" WHERE id = $1'); +Function.prototype.apply.bind(nonRaw)(null, ['UPDATE "Job" SET "status" = $1']); +const boundNonRaw = Function.prototype.call.bind(nonRaw); +boundNonRaw(null, 'DELETE FROM "AuditLog" WHERE id = $1'); +Function.prototype.call?.bind(nonRaw)(null, 'DELETE FROM "AuditLog" WHERE id = $1'); +Function.prototype["call"].bind(nonRaw)(null, 'DELETE FROM "AuditLog" WHERE id = $1'); +Reflect["apply"](nonRaw, null, ['DELETE FROM "AuditLog" WHERE id = $1']); +Reflect["ap" + "ply"](nonRaw, null, ['UPDATE "Job" SET "status" = $1']); +const bracketBoundCall = Function.prototype["call"].bind(Function.prototype["call"]); +bracketBoundCall(nonRaw, null, 'DELETE FROM "AuditLog" WHERE id = $1'); +const optionalBoundExec = Function.prototype.call.bind(nonRaw); +const optionalBoundReflectApply = Reflect.apply.bind(Reflect); +const optionalPlainReflectApply = Reflect.apply; +bracketBoundCall?.(nonRaw, null, 'DELETE FROM "AuditLog" WHERE id = $1'); +optionalBoundExec?.(null, 'DELETE FROM "AuditLog" WHERE id = $1'); +optionalBoundReflectApply?.(nonRaw, null, ['UPDATE "Job" SET "status" = $1']); +optionalPlainReflectApply?.(nonRaw, null, ['DELETE FROM "AuditLog" WHERE id = $1']); +nonRaw["call"](null, 'DELETE FROM "AuditLog" WHERE id = $1'); +nonRaw["apply"](null, ['UPDATE "Job" SET "status" = $1']); +nonRaw["bind"](null)('DELETE FROM "AuditLog" WHERE id = $1'); +const f1 = nonRaw; +const f2 = f1; +f2('DELETE FROM "AuditLog" WHERE id = $1'); +const ops = { execRaw: nonRaw }; +ops.execRaw.call(null, 'DELETE FROM "AuditLog" WHERE id = $1'); +` + }, + async (root) => { + const violations = await findS1ScopeViolations(root); + assertNoViolations(violations, "non-raw Reflect.apply and Function.prototype.call"); + } + ); + await withFixture( { "apps/api/prisma/migrations/20260602000002_job_schema_ddl/migration.sql": `