@cosmicdrift/kumiko-guards 0.388.0 → 0.390.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Warns when a `.unsafe(sql, [...])` parameter list holds a JS array (or a spread).
4
+ *
5
+ * Bun.SQL binds a JS array parameter as "a,b" on Linux, so `ANY($n)` fails with a
6
+ * malformed array literal in CI while the same call passes on macOS. Pass
7
+ * `toPostgresArrayLiteral(values)` (from `@cosmicdrift/kumiko-framework/db`) and cast the
8
+ * placeholder (`$1::text[]`).
9
+ *
10
+ * Heuristic without type info: only array literals and spreads directly inside the
11
+ * parameter-list literal are reported; array-typed variables pass unnoticed. Warning only.
12
+ */
13
+
14
+ import { readFileSync } from "node:fs";
15
+ import { relative } from "node:path";
16
+ import { Node, Project, type SourceFile, SyntaxKind } from "ts-morph";
17
+ import {
18
+ type GuardViolation,
19
+ type RepoCheck,
20
+ reportResults,
21
+ runRepoChecks,
22
+ } from "./_lib/guard-kit";
23
+ import { type ScanSpec, scanFiles } from "./_lib/scan-scope";
24
+
25
+ const SCAN: ScanSpec = { scope: "source+tests", extensions: ["ts", "tsx"] };
26
+
27
+ const UNSAFE_CALL_MARKER = ".unsafe";
28
+ const UNSAFE_PARAMS_ARG_INDEX = 1;
29
+
30
+ const FIX_HINT =
31
+ "pass toPostgresArrayLiteral(values) from @cosmicdrift/kumiko-framework/db and cast the placeholder, e.g. ANY($1::text[])";
32
+
33
+ export type UnsafeArrayParamFinding = {
34
+ readonly line: number;
35
+ readonly message: string;
36
+ };
37
+
38
+ function unwrapExpression(node: Node): Node {
39
+ let current = node;
40
+ while (
41
+ Node.isAsExpression(current) ||
42
+ Node.isSatisfiesExpression(current) ||
43
+ Node.isParenthesizedExpression(current)
44
+ ) {
45
+ current = current.getExpression();
46
+ }
47
+ return current;
48
+ }
49
+
50
+ export function findUnsafeArrayParams(sf: SourceFile): UnsafeArrayParamFinding[] {
51
+ const findings: UnsafeArrayParamFinding[] = [];
52
+ for (const call of sf.getDescendantsOfKind(SyntaxKind.CallExpression)) {
53
+ const callee = call.getExpression();
54
+ if (!Node.isPropertyAccessExpression(callee) || callee.getName() !== "unsafe") continue;
55
+ const params = call.getArguments()[UNSAFE_PARAMS_ARG_INDEX];
56
+ if (params === undefined) continue;
57
+ const paramList = unwrapExpression(params);
58
+ if (!Node.isArrayLiteralExpression(paramList)) continue;
59
+ for (const element of paramList.getElements()) {
60
+ const value = unwrapExpression(element);
61
+ if (Node.isArrayLiteralExpression(value)) {
62
+ findings.push({
63
+ line: element.getStartLineNumber(),
64
+ message: `array literal as an unsafe() parameter binds as "a,b" on Linux; ${FIX_HINT}`,
65
+ });
66
+ } else if (Node.isSpreadElement(element)) {
67
+ findings.push({
68
+ line: element.getStartLineNumber(),
69
+ message: `spread in the unsafe() parameter list; if it feeds ANY($n), ${FIX_HINT}`,
70
+ });
71
+ }
72
+ }
73
+ }
74
+ return findings;
75
+ }
76
+
77
+ export const check: RepoCheck = {
78
+ name: "Unsafe Array Param Guard",
79
+ hint:
80
+ "Bun.SQL binds a JS array parameter as 'a,b' on Linux (CI) but not on macOS. " +
81
+ "Use toPostgresArrayLiteral(values) with a $n::type[] cast in unsafe() calls.",
82
+ run(roots) {
83
+ const paths = scanFiles(SCAN, roots);
84
+ const project = new Project({ skipAddingFilesFromTsConfig: true, useInMemoryFileSystem: true });
85
+ const warnings: GuardViolation[] = [];
86
+ for (const absPath of paths) {
87
+ const text = readFileSync(absPath, "utf-8");
88
+ if (!text.includes(UNSAFE_CALL_MARKER)) continue;
89
+ const sf = project.createSourceFile(absPath, text, { overwrite: true });
90
+ for (const finding of findUnsafeArrayParams(sf)) {
91
+ warnings.push({
92
+ file: relative(process.cwd(), absPath),
93
+ line: finding.line,
94
+ message: finding.message,
95
+ });
96
+ }
97
+ }
98
+ return {
99
+ violations: [],
100
+ warnings: warnings.length > 0 ? warnings : undefined,
101
+ matchedFiles: paths.length,
102
+ notApplicable: false,
103
+ };
104
+ },
105
+ };
106
+
107
+ if (import.meta.main) {
108
+ const failed = reportResults(await runRepoChecks([check]));
109
+ process.exit(failed > 0 ? 1 : 0);
110
+ }
package/src/pre-push.sh CHANGED
@@ -151,13 +151,25 @@ fi
151
151
  # canonical sibling path instead of this worktree — checking the wrong
152
152
  # code. Run the worktree-local check only when scripts/check-wt.sh is
153
153
  # tracked and executable (untracked/symlink stubs must not hijack push).
154
- # A check-wt.sh that wraps this hook sets KUMIKO_CHECK_WT=1 so it is not re-entered.
155
- if [ "${KUMIKO_CHECK_WT:-0}" != "1" ] \
154
+ # Re-entry from the wrapping check-wt.sh is detected by process ancestry: the
155
+ # hook records its own PID in KUMIKO_CHECK_WT_OWNER before delegating, so a
156
+ # freely exported variable cannot skip the tracked check-wt.sh.
157
+ is_reentry_from_check_wt() {
158
+ reentry_pid=$$
159
+ case "${KUMIKO_CHECK_WT_OWNER:-}" in '' | *[!0-9]*) return 1 ;; esac
160
+ while [ -n "$reentry_pid" ] && [ "$reentry_pid" -gt 1 ]; do
161
+ [ "$reentry_pid" = "$KUMIKO_CHECK_WT_OWNER" ] && return 0
162
+ reentry_pid=$(ps -o ppid= -p "$reentry_pid" 2>/dev/null | tr -d ' ')
163
+ done
164
+ return 1
165
+ }
166
+
167
+ if ! is_reentry_from_check_wt \
156
168
  && [ "$(git rev-parse --path-format=absolute --git-dir)" != "$GIT_COMMON_DIR" ] \
157
169
  && git -C "$REPO_ROOT" ls-files --error-unmatch scripts/check-wt.sh >/dev/null 2>&1 \
158
170
  && [ -x "$REPO_ROOT/scripts/check-wt.sh" ]; then
159
171
  echo "[pre-push] worktree detected — worktree check (see scripts/check-wt.sh)…"
160
- cd "$REPO_ROOT" && exec "$REPO_ROOT/scripts/check-wt.sh"
172
+ cd "$REPO_ROOT" && KUMIKO_CHECK_WT_OWNER=$$ exec "$REPO_ROOT/scripts/check-wt.sh"
161
173
  fi
162
174
 
163
175
  # Runs a guard bin from the worktree's or the parent's node_modules/.bin
package/src/run-guards.ts CHANGED
@@ -38,15 +38,19 @@ import { guard as i18nKeys } from "./guard-i18n-keys";
38
38
  import { guard as i18nLocaleMount } from "./guard-i18n-locale-mount";
39
39
  import { guard as i18nLocaleTerminology } from "./guard-i18n-locale-terminology";
40
40
  import { guard as importExtensions } from "./guard-import-extensions";
41
+ import { guard as internalSubpathImports } from "./guard-internal-subpath-imports";
41
42
  import { guard as libTestCoverage } from "./guard-lib-test-coverage";
42
43
  import { guard as noDateApi } from "./guard-no-date-api";
43
44
  import { guard as noDirectFs } from "./guard-no-direct-fs";
44
45
  import { guard as noLogicInViews } from "./guard-no-logic-in-views";
45
46
  import { guard as noTemporalPolyfillImport } from "./guard-no-temporal-polyfill-import";
46
47
  import { guard as restrictedSymbols } from "./guard-restricted-symbols";
48
+ import { guard as rlsSessionSettings } from "./guard-rls-session-settings";
47
49
  import { guard as screenConventions } from "./guard-screen-conventions";
48
50
  import { guard as silentSkip } from "./guard-silent-skip";
49
51
  import { guard as tenantEscalation } from "./guard-tenant-escalation";
52
+ import { guard as testEsFixtures } from "./guard-test-es-fixtures";
53
+ import { guard as testEsRowWrites } from "./guard-test-es-row-writes";
50
54
  import { guard as testTemplateDrift } from "./guard-test-template-drift";
51
55
  import { guard as testTimeouts } from "./guard-test-timeouts";
52
56
  import { guard as unsafeJsonParse } from "./guard-unsafe-json-parse";
@@ -66,6 +70,8 @@ export const GUARDS = [
66
70
  noDateApi,
67
71
  noTemporalPolyfillImport,
68
72
  restrictedSymbols,
73
+ internalSubpathImports,
74
+ rlsSessionSettings,
69
75
  fakeTests,
70
76
  noLogicInViews,
71
77
  errorReasons,
@@ -81,6 +87,8 @@ export const GUARDS = [
81
87
  appFeatureStructure,
82
88
  libTestCoverage,
83
89
  testTimeouts,
90
+ testEsFixtures,
91
+ testEsRowWrites,
84
92
  testTemplateDrift,
85
93
  fileSize,
86
94
  commentIssueRefOnly,
@@ -15,8 +15,10 @@ import { check as noDirectProcessEnv } from "./guard-no-direct-process-env";
15
15
  import { check as primitivesDiscipline } from "./guard-primitives-discipline";
16
16
  import { check as rawSql } from "./guard-raw-sql";
17
17
  import { check as rendererBoundaries } from "./guard-renderer-boundaries";
18
+ import { check as schemaCliFeatures } from "./guard-schema-cli-features";
18
19
  import { check as testStackDrift } from "./guard-test-stack-drift";
19
20
  import { check as thinWrappers } from "./guard-thin-wrappers";
21
+ import { check as unsafeArrayParam } from "./guard-unsafe-array-param";
20
22
  import { check as upgradeState } from "./guard-upgrade-state";
21
23
 
22
24
  export const REPO_CHECKS = [
@@ -35,6 +37,8 @@ export const REPO_CHECKS = [
35
37
  agentDocsDrift,
36
38
  subpathExports,
37
39
  guardsDocDrift,
40
+ unsafeArrayParam,
41
+ schemaCliFeatures,
38
42
  ];
39
43
 
40
44
  export const REPO_CHECK_FLAGS: readonly string[] = ["--write-baseline"];