@cosmicdrift/kumiko-guards 0.360.0 → 0.361.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.
- package/package.json +2 -2
- package/src/_lib/push-root.ts +25 -0
- package/src/_lib/roots.ts +18 -1
- package/src/changes.json +6 -0
- package/src/guard-comment-lang.ts +50 -8
- package/src/index.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-guards",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.361.0",
|
|
4
4
|
"description": "AST-based security guards for Kumiko repos: direct-fs/fetch, tenant escalation, admin-API, escape hatches and related checks, run over a shared ts-morph project.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"kumiko-pre-push": "./src/pre-push.sh"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@cosmicdrift/kumiko-repo-manifest": "0.
|
|
31
|
+
"@cosmicdrift/kumiko-repo-manifest": "0.361.0",
|
|
32
32
|
"ts-morph": "^28.0.0"
|
|
33
33
|
},
|
|
34
34
|
"publishConfig": {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { basename } from "node:path";
|
|
2
|
+
import { findLocalRepo, localRepoDirName } from "./roots";
|
|
3
|
+
|
|
4
|
+
// A worktree's folder name proves nothing (`kumiko-*` siblings share prefixes), so it is judged by its canonical checkout's directory name.
|
|
5
|
+
function pushRootMatchesCliScope(pushRepoRoot: string, cliScope: string): boolean {
|
|
6
|
+
if (basename(pushRepoRoot) === cliScope) return true;
|
|
7
|
+
const local = findLocalRepo(pushRepoRoot);
|
|
8
|
+
return local !== undefined && (local.name === cliScope || localRepoDirName(local) === cliScope);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// A push root left over from an earlier hook run or a debug session must not redirect a guard at the wrong repo, so it only counts when KUMIKO_CLI_SCOPE names it.
|
|
12
|
+
export function resolvePushRepoRoot(
|
|
13
|
+
vars: Readonly<Record<string, string | undefined>>,
|
|
14
|
+
): string | undefined {
|
|
15
|
+
const cliScope = vars["KUMIKO_CLI_SCOPE"]?.trim();
|
|
16
|
+
const pushRepoRoot = vars["KUMIKO_PUSH_REPO_ROOT"]?.trim();
|
|
17
|
+
if (!cliScope || !pushRepoRoot) return undefined;
|
|
18
|
+
if (!pushRootMatchesCliScope(pushRepoRoot, cliScope)) {
|
|
19
|
+
console.error(
|
|
20
|
+
`[push-root] KUMIKO_PUSH_REPO_ROOT="${pushRepoRoot}" does not match KUMIKO_CLI_SCOPE="${cliScope}" — ignoring inherited/stale push root.`,
|
|
21
|
+
);
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
return pushRepoRoot;
|
|
25
|
+
}
|
package/src/_lib/roots.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { execFileSync } from "node:child_process";
|
|
11
11
|
import { existsSync, lstatSync, readdirSync, readFileSync, realpathSync } from "node:fs";
|
|
12
|
-
import { dirname, join, resolve } from "node:path";
|
|
12
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
13
13
|
import {
|
|
14
14
|
type LoadedRepoManifest,
|
|
15
15
|
loadRepoManifest,
|
|
@@ -115,6 +115,23 @@ function gitToplevelOf(cwd: string): string | undefined {
|
|
|
115
115
|
return spelled;
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
/**
|
|
119
|
+
* The main checkout of the repo containing `from`, independent of what a linked
|
|
120
|
+
* worktree's folder is called. Undefined when git cannot answer or the common
|
|
121
|
+
* dir is not a `<repo>/.git`.
|
|
122
|
+
*/
|
|
123
|
+
function canonicalCheckoutOf(from: string): string | undefined {
|
|
124
|
+
const commonDir = git(from, ["rev-parse", "--path-format=absolute", "--git-common-dir"]);
|
|
125
|
+
if (commonDir === undefined || basename(commonDir) !== ".git") return undefined;
|
|
126
|
+
const checkout = dirname(commonDir);
|
|
127
|
+
return existsSync(checkout) ? checkout : undefined;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Directory name of the repo's canonical checkout; `local.name` when git names none. */
|
|
131
|
+
export function localRepoDirName(local: RepoRoot): string {
|
|
132
|
+
return basename(canonicalCheckoutOf(local.absPath) ?? local.name);
|
|
133
|
+
}
|
|
134
|
+
|
|
118
135
|
function rootFrom(name: string, dir: string, loaded: LoadedRepoManifest): RepoRoot {
|
|
119
136
|
return {
|
|
120
137
|
name,
|
package/src/changes.json
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.361.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "comment-lang guard scans tooling repos, handles division before a comment and ignores a stale KUMIKO_PUSH_REPO_ROOT",
|
|
6
|
+
"detail": "comment-lang scans tooling repos, no longer rescans a division as a regex literal, and honours KUMIKO_PUSH_REPO_ROOT only when KUMIKO_CLI_SCOPE names that checkout. The scan-scope helpers scanFiles and scanRoots are exported."
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.360.0",
|
|
4
10
|
"type": "fix",
|
|
@@ -2,8 +2,9 @@ import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { ts } from "ts-morph";
|
|
4
4
|
import { gitEnv } from "./_lib/git-env";
|
|
5
|
-
import { cliFlagsError, isLocalFinding } from "./_lib/guard-kit";
|
|
5
|
+
import { ALL_REPO_KINDS, cliFlagsError, isLocalFinding } from "./_lib/guard-kit";
|
|
6
6
|
import { lineHasIgnoreTag } from "./_lib/ignore-tag";
|
|
7
|
+
import { resolvePushRepoRoot } from "./_lib/push-root";
|
|
7
8
|
import { resolveRepoRoots } from "./_lib/roots";
|
|
8
9
|
import { type ScanSpec, scanFiles } from "./_lib/scan-scope";
|
|
9
10
|
|
|
@@ -28,6 +29,7 @@ const IGNORE_TAG = "kumiko-lint-ignore comment-lang";
|
|
|
28
29
|
|
|
29
30
|
const SCAN: ScanSpec = {
|
|
30
31
|
scope: "source",
|
|
32
|
+
kinds: ALL_REPO_KINDS,
|
|
31
33
|
extensions: ["ts", "tsx"],
|
|
32
34
|
frameworkWithin: ["packages/*/src/**"],
|
|
33
35
|
};
|
|
@@ -66,6 +68,48 @@ function germanCommentSite(
|
|
|
66
68
|
};
|
|
67
69
|
}
|
|
68
70
|
|
|
71
|
+
// After these tokens a `/` is division, not the start of a regex literal.
|
|
72
|
+
const DIVISION_PRECEDING_KINDS: ReadonlySet<ts.SyntaxKind> = new Set([
|
|
73
|
+
ts.SyntaxKind.Identifier,
|
|
74
|
+
ts.SyntaxKind.ThisKeyword,
|
|
75
|
+
ts.SyntaxKind.TrueKeyword,
|
|
76
|
+
ts.SyntaxKind.FalseKeyword,
|
|
77
|
+
ts.SyntaxKind.NullKeyword,
|
|
78
|
+
ts.SyntaxKind.NumericLiteral,
|
|
79
|
+
ts.SyntaxKind.BigIntLiteral,
|
|
80
|
+
ts.SyntaxKind.StringLiteral,
|
|
81
|
+
ts.SyntaxKind.NoSubstitutionTemplateLiteral,
|
|
82
|
+
ts.SyntaxKind.TemplateTail,
|
|
83
|
+
ts.SyntaxKind.RegularExpressionLiteral,
|
|
84
|
+
ts.SyntaxKind.CloseParenToken,
|
|
85
|
+
ts.SyntaxKind.CloseBracketToken,
|
|
86
|
+
ts.SyntaxKind.CloseBraceToken,
|
|
87
|
+
ts.SyntaxKind.PlusPlusToken,
|
|
88
|
+
ts.SyntaxKind.MinusMinusToken,
|
|
89
|
+
]);
|
|
90
|
+
|
|
91
|
+
const TRIVIA_KINDS: ReadonlySet<ts.SyntaxKind> = new Set([
|
|
92
|
+
ts.SyntaxKind.SingleLineCommentTrivia,
|
|
93
|
+
ts.SyntaxKind.MultiLineCommentTrivia,
|
|
94
|
+
ts.SyntaxKind.WhitespaceTrivia,
|
|
95
|
+
ts.SyntaxKind.NewLineTrivia,
|
|
96
|
+
]);
|
|
97
|
+
|
|
98
|
+
// The raw scanner always yields SlashToken for `/`; without the rescan a regex
|
|
99
|
+
// body like /\{/ would shift braceDepth and end a template early. After an
|
|
100
|
+
// operand the rescan would instead swallow `/ 2; // {` as a regex and hide the comment.
|
|
101
|
+
function rescanSlashAsRegex(
|
|
102
|
+
scanner: ts.Scanner,
|
|
103
|
+
kind: ts.SyntaxKind,
|
|
104
|
+
previousSignificant: ts.SyntaxKind | undefined,
|
|
105
|
+
): ts.SyntaxKind {
|
|
106
|
+
if (previousSignificant !== undefined && DIVISION_PRECEDING_KINDS.has(previousSignificant)) {
|
|
107
|
+
return kind;
|
|
108
|
+
}
|
|
109
|
+
const reScanned = scanner.reScanSlashToken();
|
|
110
|
+
return reScanned === ts.SyntaxKind.RegularExpressionLiteral ? reScanned : kind;
|
|
111
|
+
}
|
|
112
|
+
|
|
69
113
|
function closesTemplateSubstitution(
|
|
70
114
|
templateBraceDepths: readonly number[],
|
|
71
115
|
braceDepth: number,
|
|
@@ -91,6 +135,7 @@ export function scanGermanComments(fullText: string, file: string): Site[] {
|
|
|
91
135
|
|
|
92
136
|
const templateBraceDepths: number[] = [];
|
|
93
137
|
let braceDepth = 0;
|
|
138
|
+
let previousSignificant: ts.SyntaxKind | undefined;
|
|
94
139
|
|
|
95
140
|
let kind = scanner.scan();
|
|
96
141
|
while (kind !== ts.SyntaxKind.EndOfFileToken) {
|
|
@@ -107,13 +152,9 @@ export function scanGermanComments(fullText: string, file: string): Site[] {
|
|
|
107
152
|
break;
|
|
108
153
|
}
|
|
109
154
|
case ts.SyntaxKind.SlashToken:
|
|
110
|
-
case ts.SyntaxKind.SlashEqualsToken:
|
|
111
|
-
|
|
112
|
-
// regex body like /\{/ would shift braceDepth and end a template early.
|
|
113
|
-
const reScanned = scanner.reScanSlashToken();
|
|
114
|
-
if (reScanned === ts.SyntaxKind.RegularExpressionLiteral) kind = reScanned;
|
|
155
|
+
case ts.SyntaxKind.SlashEqualsToken:
|
|
156
|
+
kind = rescanSlashAsRegex(scanner, kind, previousSignificant);
|
|
115
157
|
break;
|
|
116
|
-
}
|
|
117
158
|
case ts.SyntaxKind.TemplateHead:
|
|
118
159
|
case ts.SyntaxKind.TemplateMiddle:
|
|
119
160
|
templateBraceDepths.push(braceDepth);
|
|
@@ -132,6 +173,7 @@ export function scanGermanComments(fullText: string, file: string): Site[] {
|
|
|
132
173
|
default:
|
|
133
174
|
break;
|
|
134
175
|
}
|
|
176
|
+
if (!TRIVIA_KINDS.has(kind)) previousSignificant = kind;
|
|
135
177
|
kind = scanner.scan();
|
|
136
178
|
}
|
|
137
179
|
return sites;
|
|
@@ -422,7 +464,7 @@ export function runCommentLangCli(
|
|
|
422
464
|
return 1;
|
|
423
465
|
}
|
|
424
466
|
|
|
425
|
-
const pushRoot = env
|
|
467
|
+
const pushRoot = resolvePushRepoRoot(env);
|
|
426
468
|
const root = pushRoot && existsSync(pushRoot) ? pushRoot : cwd;
|
|
427
469
|
|
|
428
470
|
try {
|
package/src/index.ts
CHANGED
|
@@ -21,6 +21,7 @@ export {
|
|
|
21
21
|
type SuiteInventory,
|
|
22
22
|
} from "./_lib/guard-kit";
|
|
23
23
|
export { findLocalRepo, type RepoRoot, resolveRepoRoots } from "./_lib/roots";
|
|
24
|
+
export { type RootScan, type ScanOptions, scanFiles, scanRoots } from "./_lib/scan-scope";
|
|
24
25
|
export {
|
|
25
26
|
loadSecurityBaseline,
|
|
26
27
|
parseSecurityBaseline,
|