@fairfox/polly 0.22.0 → 0.24.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/README.md +55 -1
- package/dist/src/elysia/index.js +5 -3
- package/dist/src/elysia/index.js.map +3 -3
- package/dist/src/elysia/peer-repo-plugin.d.ts +1 -1
- package/dist/src/mesh-node.d.ts +89 -0
- package/dist/src/mesh-node.js +594 -0
- package/dist/src/mesh-node.js.map +14 -0
- package/dist/src/mesh.d.ts +10 -0
- package/dist/src/mesh.js +926 -24
- package/dist/src/mesh.js.map +17 -9
- package/dist/src/peer.d.ts +1 -0
- package/dist/src/peer.js +108 -85
- package/dist/src/peer.js.map +11 -10
- package/dist/src/shared/lib/blob-cache.d.ts +58 -0
- package/dist/src/shared/lib/blob-store-impl.d.ts +33 -0
- package/dist/src/shared/lib/blob-store.d.ts +87 -0
- package/dist/src/shared/lib/blob-transfer.d.ts +58 -0
- package/dist/src/shared/lib/crdt-specialised.d.ts +1 -1
- package/dist/src/shared/lib/crdt-state.d.ts +1 -1
- package/dist/src/shared/lib/keyring-storage.d.ts +57 -0
- package/dist/src/shared/lib/mesh-client.d.ts +91 -0
- package/dist/src/shared/lib/mesh-network-adapter.d.ts +1 -1
- package/dist/src/shared/lib/mesh-signaling-client.d.ts +6 -0
- package/dist/src/shared/lib/mesh-state.d.ts +1 -1
- package/dist/src/shared/lib/mesh-webrtc-adapter.d.ts +20 -1
- package/dist/src/shared/lib/peer-relay-adapter.d.ts +1 -1
- package/dist/src/shared/lib/peer-repo-server.d.ts +1 -1
- package/dist/src/shared/lib/peer-state.d.ts +1 -1
- package/dist/src/shared/lib/wasm-init.d.ts +17 -0
- package/dist/tools/quality/src/cli.js +98 -47
- package/dist/tools/quality/src/cli.js.map +4 -4
- package/dist/tools/quality/src/index.d.ts +25 -0
- package/dist/tools/quality/src/index.js +196 -0
- package/dist/tools/quality/src/index.js.map +10 -0
- package/dist/tools/quality/src/no-as-casting.d.ts +44 -0
- package/package.json +22 -2
|
@@ -42,28 +42,55 @@ function isLineClean(line) {
|
|
|
42
42
|
}
|
|
43
43
|
if (line.match(/\bas\s*[=:,]/))
|
|
44
44
|
return true;
|
|
45
|
-
if (line
|
|
45
|
+
if (everyAsInsideString(line))
|
|
46
|
+
return true;
|
|
47
|
+
if (isJsxText(trimmed))
|
|
48
|
+
return true;
|
|
49
|
+
if (isPlainText(trimmed))
|
|
46
50
|
return true;
|
|
47
|
-
const idx = line.indexOf(" as ");
|
|
48
|
-
if (idx >= 0) {
|
|
49
|
-
const before = line.substring(0, idx);
|
|
50
|
-
const singleQuotes = (before.match(/'/g) ?? []).length;
|
|
51
|
-
const doubleQuotes = (before.match(/"/g) ?? []).length;
|
|
52
|
-
const backticks = (before.match(/`/g) ?? []).length;
|
|
53
|
-
if (singleQuotes % 2 === 1 || doubleQuotes % 2 === 1 || backticks % 2 === 1) {
|
|
54
|
-
return true;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
51
|
const commentIdx = line.indexOf("//");
|
|
58
52
|
if (commentIdx >= 0 && line.indexOf(" as ", commentIdx) >= 0) {
|
|
59
53
|
const beforeComment = line.substring(0, commentIdx);
|
|
60
54
|
if (!beforeComment.includes(" as "))
|
|
61
55
|
return true;
|
|
62
56
|
}
|
|
57
|
+
if (line.match(/"\)\s+as\s+\w+"/))
|
|
58
|
+
return true;
|
|
63
59
|
if (line.includes(" satisfies "))
|
|
64
60
|
return true;
|
|
65
61
|
return false;
|
|
66
62
|
}
|
|
63
|
+
function everyAsInsideString(line) {
|
|
64
|
+
let searchFrom = 0;
|
|
65
|
+
while (true) {
|
|
66
|
+
const idx = line.indexOf(" as ", searchFrom);
|
|
67
|
+
if (idx < 0)
|
|
68
|
+
return true;
|
|
69
|
+
const before = line.substring(0, idx);
|
|
70
|
+
const singleQuotes = (before.match(/'/g) ?? []).length;
|
|
71
|
+
const doubleQuotes = (before.match(/"/g) ?? []).length;
|
|
72
|
+
const backticks = (before.match(/`/g) ?? []).length;
|
|
73
|
+
if (singleQuotes % 2 === 0 && doubleQuotes % 2 === 0 && backticks % 2 === 0) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
searchFrom = idx + 4;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function isJsxText(trimmed) {
|
|
80
|
+
if (trimmed.match(/^[^{};=()]*\bas\b[^{};=()]*$/)) {
|
|
81
|
+
if (!trimmed.match(/\bas\s+[A-Z]\w*/) && !trimmed.match(/\bas\s+(string|number|boolean|any|unknown|never)\b/)) {
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
function isPlainText(trimmed) {
|
|
88
|
+
const idx = trimmed.indexOf(" as ");
|
|
89
|
+
if (idx < 0)
|
|
90
|
+
return false;
|
|
91
|
+
const before = trimmed.substring(0, idx);
|
|
92
|
+
return !before.match(/[={}:;(]/) && !before.match(/\b(const|let|var|type|interface|function|return|await)\b/);
|
|
93
|
+
}
|
|
67
94
|
function suggestFix(line) {
|
|
68
95
|
if (line.includes("JSON.parse")) {
|
|
69
96
|
return "Use a validation function or type guard to parse and validate the result.";
|
|
@@ -94,52 +121,72 @@ function suggestFix(line) {
|
|
|
94
121
|
}
|
|
95
122
|
return;
|
|
96
123
|
}
|
|
124
|
+
function isFileExcluded(relative, excludeDirs, excludePackages, excludeFiles) {
|
|
125
|
+
const segments = relative.split("/");
|
|
126
|
+
if (segments.some((s) => excludeDirs.has(s)))
|
|
127
|
+
return true;
|
|
128
|
+
if (excludePackages.size > 0 && segments.some((s) => excludePackages.has(s)))
|
|
129
|
+
return true;
|
|
130
|
+
const basename = segments[segments.length - 1] ?? "";
|
|
131
|
+
return excludeFiles.has(basename) || excludeFiles.has(relative);
|
|
132
|
+
}
|
|
133
|
+
function findViolations(relative, content) {
|
|
134
|
+
const results = [];
|
|
135
|
+
const lines = content.split(`
|
|
136
|
+
`);
|
|
137
|
+
let insideTemplate = false;
|
|
138
|
+
for (let i = 0;i < lines.length; i++) {
|
|
139
|
+
const line = lines[i] ?? "";
|
|
140
|
+
const backticks = (line.match(/`/g) ?? []).length;
|
|
141
|
+
const startedInTemplate = insideTemplate;
|
|
142
|
+
if (backticks % 2 === 1)
|
|
143
|
+
insideTemplate = !insideTemplate;
|
|
144
|
+
if (startedInTemplate && backticks === 0 && !line.includes("${"))
|
|
145
|
+
continue;
|
|
146
|
+
if (!isLineClean(line)) {
|
|
147
|
+
results.push({
|
|
148
|
+
file: relative,
|
|
149
|
+
line: i + 1,
|
|
150
|
+
content: line.trim(),
|
|
151
|
+
advice: suggestFix(line.trim())
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return results;
|
|
156
|
+
}
|
|
157
|
+
function printViolations(violations) {
|
|
158
|
+
if (violations.length === 0) {
|
|
159
|
+
console.log("[no-as-casting] ✅ No violations found.");
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
console.log(`[no-as-casting] ❌ ${violations.length} violation(s) found:
|
|
163
|
+
`);
|
|
164
|
+
for (const v of violations) {
|
|
165
|
+
console.log(` ${v.file}:${v.line}`);
|
|
166
|
+
console.log(` ${v.content}`);
|
|
167
|
+
if (v.advice)
|
|
168
|
+
console.log(` \uD83D\uDCA1 ${v.advice}`);
|
|
169
|
+
console.log();
|
|
170
|
+
}
|
|
171
|
+
console.log("[no-as-casting] Use type guards, validation, or fix the types at the source.");
|
|
172
|
+
console.log('[no-as-casting] Only "as const" and "as unknown as" are allowed.');
|
|
173
|
+
}
|
|
97
174
|
async function checkNoAsCasting(options) {
|
|
98
175
|
const rootDir = options.rootDir;
|
|
99
176
|
const excludeDirs = new Set(options.exclude ?? ["node_modules", "dist", ".git", ".bun"]);
|
|
177
|
+
const excludePackages = new Set(options.excludePackages ?? []);
|
|
178
|
+
const excludeFiles = new Set(options.excludeFiles ?? []);
|
|
100
179
|
const pattern = options.filePatterns ?? "**/*.{ts,tsx}";
|
|
101
180
|
const glob = new Glob(pattern);
|
|
102
181
|
const violations = [];
|
|
103
182
|
for await (const file of glob.scan({ cwd: rootDir, absolute: true })) {
|
|
104
183
|
const relative = file.replace(`${rootDir}/`, "");
|
|
105
|
-
|
|
106
|
-
if (segments.some((s) => excludeDirs.has(s)))
|
|
184
|
+
if (isFileExcluded(relative, excludeDirs, excludePackages, excludeFiles))
|
|
107
185
|
continue;
|
|
108
186
|
const content = readFileSync(file, "utf-8");
|
|
109
|
-
|
|
110
|
-
`);
|
|
111
|
-
for (let i = 0;i < lines.length; i++) {
|
|
112
|
-
const line = lines[i] ?? "";
|
|
113
|
-
if (!isLineClean(line)) {
|
|
114
|
-
violations.push({
|
|
115
|
-
file: relative,
|
|
116
|
-
line: i + 1,
|
|
117
|
-
content: line.trim(),
|
|
118
|
-
advice: suggestFix(line.trim())
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
}
|
|
187
|
+
violations.push(...findViolations(relative, content));
|
|
122
188
|
}
|
|
123
|
-
return {
|
|
124
|
-
violations,
|
|
125
|
-
print() {
|
|
126
|
-
if (violations.length === 0) {
|
|
127
|
-
console.log("[no-as-casting] ✅ No violations found.");
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
console.log(`[no-as-casting] ❌ ${violations.length} violation(s) found:
|
|
131
|
-
`);
|
|
132
|
-
for (const v of violations) {
|
|
133
|
-
console.log(` ${v.file}:${v.line}`);
|
|
134
|
-
console.log(` ${v.content}`);
|
|
135
|
-
if (v.advice)
|
|
136
|
-
console.log(` \uD83D\uDCA1 ${v.advice}`);
|
|
137
|
-
console.log();
|
|
138
|
-
}
|
|
139
|
-
console.log("[no-as-casting] Use type guards, validation, or fix the types at the source.");
|
|
140
|
-
console.log('[no-as-casting] Only "as const" and "as unknown as" are allowed.');
|
|
141
|
-
}
|
|
142
|
-
};
|
|
189
|
+
return { violations, print: () => printViolations(violations) };
|
|
143
190
|
}
|
|
144
191
|
|
|
145
192
|
// tools/quality/src/cli.ts
|
|
@@ -150,13 +197,17 @@ function getFlag(name) {
|
|
|
150
197
|
}
|
|
151
198
|
var rootDir = getFlag("root") ?? process.cwd();
|
|
152
199
|
var exclude = getFlag("exclude")?.split(",") ?? ["node_modules", "dist", ".git", ".bun"];
|
|
200
|
+
var excludePackages = getFlag("exclude-packages")?.split(",");
|
|
201
|
+
var excludeFiles = getFlag("exclude-files")?.split(",");
|
|
153
202
|
var filePatterns = getFlag("pattern");
|
|
154
203
|
var result = await checkNoAsCasting({
|
|
155
204
|
rootDir,
|
|
156
205
|
exclude,
|
|
206
|
+
...excludePackages ? { excludePackages } : {},
|
|
207
|
+
...excludeFiles ? { excludeFiles } : {},
|
|
157
208
|
...filePatterns ? { filePatterns } : {}
|
|
158
209
|
});
|
|
159
210
|
result.print();
|
|
160
211
|
process.exit(result.violations.length > 0 ? 1 : 0);
|
|
161
212
|
|
|
162
|
-
//# debugId=
|
|
213
|
+
//# debugId=D4F70D746137C24364756E2164756E21
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../tools/quality/src/no-as-casting.ts", "../tools/quality/src/cli.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"/**\n * No-as-casting conformance check.\n *\n * Bans all TypeScript type assertions (`as Type`) except the allowed\n * patterns: `as const` (literal narrowing), `as unknown as` (explicit\n * escape hatch), import/export renames, and `as` inside strings or\n * comments. Violations include pattern-specific fix advice.\n *\n * This module exports the check logic as a library so consuming\n * applications can import it from `@fairfox/polly/quality` and run it\n * programmatically. Polly's own `scripts/check-no-as-casting.ts` is a\n * thin CLI wrapper around these exports.\n */\n\nimport { readFileSync } from \"node:fs\";\nimport { Glob } from \"bun\";\n\nexport interface Violation {\n file: string;\n line: number;\n content: string;\n advice?: string;\n}\n\nexport interface CheckResult {\n violations: Violation[];\n print: () => void;\n}\n\nexport interface CheckOptions {\n rootDir: string;\n exclude?: string[];\n filePatterns?: string;\n}\n\n/**\n * Check whether a line contains a forbidden `as` type assertion.\n * Returns true if the line is clean (no violation), false if it violates.\n */\n// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Source-text scanning with many skip rules is inherently branchy.\nexport function isLineClean(line: string): boolean {\n if (!line.includes(\" as \")) return true;\n\n const trimmed = line.trim();\n\n if (trimmed.startsWith(\"//\") || trimmed.startsWith(\"*\") || trimmed.startsWith(\"/*\")) {\n return true;\n }\n\n if (line.match(/\\bas const\\b/)) {\n const withoutAsConst = line.replace(/\\bas const\\b/g, \"\");\n if (!withoutAsConst.includes(\" as \")) return true;\n }\n\n if (line.includes(\" as unknown as \") || line.trimEnd().endsWith(\"as unknown as\")) {\n const withoutEscapeHatch = line.replace(/\\bas unknown as\\b/g, \"\");\n if (!withoutEscapeHatch.includes(\" as \")) return true;\n }\n\n if (\n line.match(/\\b(import|export)\\s+.*\\s+as\\s+\\w+/) ||\n line.match(/\\b(import|export)\\s+\\*\\s+as\\s+\\w+/) ||\n line.match(/\\b(import|export)\\s+type\\s+.*\\s+as\\s+\\w+/) ||\n line.match(/^\\s*\\w+\\s+as\\s+\\w+,?\\s*$/) ||\n line.match(/^\\s*type\\s+\\w+\\s+as\\s+\\w+,?\\s*$/)\n ) {\n return true;\n }\n\n if (line.match(/\\bas\\s*[=:,]/)) return true;\n if (line.match(/\\)\\s+as\\s+\\w+/)) return true;\n\n const idx = line.indexOf(\" as \");\n if (idx >= 0) {\n const before = line.substring(0, idx);\n const singleQuotes = (before.match(/'/g) ?? []).length;\n const doubleQuotes = (before.match(/\"/g) ?? []).length;\n const backticks = (before.match(/`/g) ?? []).length;\n if (singleQuotes % 2 === 1 || doubleQuotes % 2 === 1 || backticks % 2 === 1) {\n return true;\n }\n }\n\n const commentIdx = line.indexOf(\"//\");\n if (commentIdx >= 0 && line.indexOf(\" as \", commentIdx) >= 0) {\n const beforeComment = line.substring(0, commentIdx);\n if (!beforeComment.includes(\" as \")) return true;\n }\n\n if (line.includes(\" satisfies \")) return true;\n\n return false;\n}\n\n/**\n * Suggest a concrete fix for a specific violation pattern.\n */\n// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Pattern-matching advice is a linear chain of if-returns.\nexport function suggestFix(line: string): string | undefined {\n if (line.includes(\"JSON.parse\")) {\n return \"Use a validation function or type guard to parse and validate the result.\";\n }\n if (\n line.includes(\"as HTMLInputElement\") ||\n line.includes(\"as HTMLTextAreaElement\") ||\n line.includes(\"as HTMLButtonElement\")\n ) {\n return \"Use instanceof: if (el instanceof HTMLInputElement) { el.value ... }\";\n }\n if (line.includes(\"as HTMLElement\") || line.includes(\"as Element\")) {\n return \"Use instanceof: if (el instanceof HTMLElement) { ... }\";\n }\n if (line.includes(\".doc()\") && line.includes(\"as \")) {\n return \"Type the DocHandle generic: repo.find<MyType>(id) returns DocHandle<MyType>.\";\n }\n if (\n line.includes(\"Record<string, unknown>\") &&\n (line.includes(\"window\") || line.includes(\"globalThis\"))\n ) {\n return \"Extract a type guard: function getGlobalProp(name: string): unknown { ... }\";\n }\n if (line.includes(\"Record<string, unknown>\")) {\n return \"Use a type guard function that narrows the unknown value to the target shape.\";\n }\n if (line.includes(\"as PeerId\") || line.includes(\"as DocumentId\")) {\n return \"Use the library's branded-type constructor if available, or centralise the cast in a factory.\";\n }\n if (line.includes(\"as string\") || line.includes(\"as number\") || line.includes(\"as boolean\")) {\n return \"Narrow with typeof: if (typeof x === 'string') { ... }\";\n }\n if (line.includes(\"as any\")) {\n return \"Replace 'any' with 'unknown' and add a type guard or validation at the boundary.\";\n }\n return undefined;\n}\n\n/**\n * Run the no-as-casting check against a directory. Returns a result\n * object with the violations and a print function for CLI output.\n */\nexport async function checkNoAsCasting(options: CheckOptions): Promise<CheckResult> {\n const rootDir = options.rootDir;\n const excludeDirs = new Set(options.exclude ?? [\"node_modules\", \"dist\", \".git\", \".bun\"]);\n const pattern = options.filePatterns ?? \"**/*.{ts,tsx}\";\n const glob = new Glob(pattern);\n const violations: Violation[] = [];\n\n for await (const file of glob.scan({ cwd: rootDir, absolute: true })) {\n const relative = file.replace(`${rootDir}/`, \"\");\n const segments = relative.split(\"/\");\n if (segments.some((s) => excludeDirs.has(s))) continue;\n\n const content = readFileSync(file, \"utf-8\");\n const lines = content.split(\"\\n\");\n\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i] ?? \"\";\n if (!isLineClean(line)) {\n violations.push({\n file: relative,\n line: i + 1,\n content: line.trim(),\n advice: suggestFix(line.trim()),\n });\n }\n }\n }\n\n return {\n violations,\n print() {\n if (violations.length === 0) {\n console.log(\"[no-as-casting] ✅ No violations found.\");\n return;\n }\n console.log(`[no-as-casting] ❌ ${violations.length} violation(s) found:\\n`);\n for (const v of violations) {\n console.log(` ${v.file}:${v.line}`);\n console.log(` ${v.content}`);\n if (v.advice) console.log(` 💡 ${v.advice}`);\n console.log();\n }\n console.log(\"[no-as-casting] Use type guards, validation, or fix the types at the source.\");\n console.log('[no-as-casting] Only \"as const\" and \"as unknown as\" are allowed.');\n },\n };\n}\n",
|
|
6
|
-
"#!/usr/bin/env bun\n\n/**\n * CLI entry point for Polly quality checks.\n *\n * polly quality [--root <dir>] [--exclude <dirs>] [--pattern <glob>]\n *\n * Runs all conformance checks (currently: no-as-casting) against the\n * target directory and exits non-zero when violations are found.\n */\n\nimport { checkNoAsCasting } from \"./no-as-casting\";\n\nconst args = process.argv.slice(2);\n\nfunction getFlag(name: string): string | undefined {\n const idx = args.indexOf(`--${name}`);\n return idx >= 0 ? args[idx + 1] : undefined;\n}\n\nconst rootDir = getFlag(\"root\") ?? process.cwd();\nconst exclude = getFlag(\"exclude\")?.split(\",\") ?? [\"node_modules\", \"dist\", \".git\", \".bun\"];\nconst filePatterns = getFlag(\"pattern\");\n\nconst result = await checkNoAsCasting({\n rootDir,\n exclude,\n ...(filePatterns ? { filePatterns } : {}),\n});\n\nresult.print();\nprocess.exit(result.violations.length > 0 ? 1 : 0);\n"
|
|
5
|
+
"/**\n * No-as-casting conformance check.\n *\n * Bans all TypeScript type assertions (`as Type`) except the allowed\n * patterns: `as const` (literal narrowing), `as unknown as` (explicit\n * escape hatch), import/export renames, and `as` inside strings or\n * comments. Violations include pattern-specific fix advice.\n *\n * This module exports the check logic as a library so consuming\n * applications can import it from `@fairfox/polly/quality` and run it\n * programmatically. Polly's own `scripts/check-no-as-casting.ts` is a\n * thin CLI wrapper around these exports.\n */\n\nimport { readFileSync } from \"node:fs\";\nimport { Glob } from \"bun\";\n\nexport interface Violation {\n file: string;\n line: number;\n content: string;\n advice?: string;\n}\n\nexport interface CheckResult {\n violations: Violation[];\n print: () => void;\n}\n\nexport interface CheckOptions {\n rootDir: string;\n exclude?: string[];\n excludePackages?: string[];\n excludeFiles?: string[];\n filePatterns?: string;\n}\n\n/**\n * Check whether a line contains a forbidden `as` type assertion.\n * Returns true if the line is clean (no violation), false if it violates.\n */\n// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Source-text scanning with many skip rules is inherently branchy.\nexport function isLineClean(line: string): boolean {\n if (!line.includes(\" as \")) return true;\n\n const trimmed = line.trim();\n\n // Full-line comments\n if (trimmed.startsWith(\"//\") || trimmed.startsWith(\"*\") || trimmed.startsWith(\"/*\")) {\n return true;\n }\n\n // as const (literal narrowing)\n if (line.match(/\\bas const\\b/)) {\n const withoutAsConst = line.replace(/\\bas const\\b/g, \"\");\n if (!withoutAsConst.includes(\" as \")) return true;\n }\n\n // as unknown as (explicit escape hatch)\n if (line.includes(\" as unknown as \") || line.trimEnd().endsWith(\"as unknown as\")) {\n const withoutEscapeHatch = line.replace(/\\bas unknown as\\b/g, \"\");\n if (!withoutEscapeHatch.includes(\" as \")) return true;\n }\n\n // Import/export renames\n if (\n line.match(/\\b(import|export)\\s+.*\\s+as\\s+\\w+/) ||\n line.match(/\\b(import|export)\\s+\\*\\s+as\\s+\\w+/) ||\n line.match(/\\b(import|export)\\s+type\\s+.*\\s+as\\s+\\w+/) ||\n line.match(/^\\s*\\w+\\s+as\\s+\\w+,?\\s*$/) ||\n line.match(/^\\s*type\\s+\\w+\\s+as\\s+\\w+,?\\s*$/)\n ) {\n return true;\n }\n\n // Property declarations: as= or as: or as,\n if (line.match(/\\bas\\s*[=:,]/)) return true;\n\n // String literal detection: count quotes before each ` as ` occurrence.\n // If any quote type has an odd count, the ` as ` is inside a string.\n if (everyAsInsideString(line)) return true;\n\n // JSX text: ` as ` between > and < with no code syntax around it\n if (isJsxText(trimmed)) return true;\n\n // Plain text heuristic: indented line with no code syntax characters\n // before ` as ` — catches multiline JSX text and template literal bodies.\n if (isPlainText(trimmed)) return true;\n\n // Inline comment: ` as ` appears only after //\n const commentIdx = line.indexOf(\"//\");\n if (commentIdx >= 0 && line.indexOf(\" as \", commentIdx) >= 0) {\n const beforeComment = line.substring(0, commentIdx);\n if (!beforeComment.includes(\" as \")) return true;\n }\n\n // SQL alias: `) as column_name`\n if (line.match(/\"\\)\\s+as\\s+\\w+\"/)) return true;\n\n if (line.includes(\" satisfies \")) return true;\n\n return false;\n}\n\n/**\n * Returns true when every ` as ` occurrence in the line falls inside a\n * string literal (single-quoted, double-quoted, or backtick).\n */\nfunction everyAsInsideString(line: string): boolean {\n let searchFrom = 0;\n while (true) {\n const idx = line.indexOf(\" as \", searchFrom);\n if (idx < 0) return true; // no more ` as ` to check\n const before = line.substring(0, idx);\n const singleQuotes = (before.match(/'/g) ?? []).length;\n const doubleQuotes = (before.match(/\"/g) ?? []).length;\n const backticks = (before.match(/`/g) ?? []).length;\n if (singleQuotes % 2 === 0 && doubleQuotes % 2 === 0 && backticks % 2 === 0) {\n return false; // this ` as ` is outside any string\n }\n searchFrom = idx + 4;\n }\n}\n\n/**\n * Detects JSX text content — ` as ` appearing between > and < with no\n * code-like syntax around it (no braces, semicolons, equals signs).\n */\nfunction isJsxText(trimmed: string): boolean {\n // Classic JSX text: starts after > or is plain text ending before <\n if (trimmed.match(/^[^{};=()]*\\bas\\b[^{};=()]*$/)) {\n // No code syntax at all — could be JSX text or template literal body.\n // Reject if it looks like a type assertion (word ` as ` TypeName pattern\n // where TypeName starts with uppercase, or is a known TS type).\n if (\n !trimmed.match(/\\bas\\s+[A-Z]\\w*/) &&\n !trimmed.match(/\\bas\\s+(string|number|boolean|any|unknown|never)\\b/)\n ) {\n return true;\n }\n }\n return false;\n}\n\n/**\n * Heuristic for plain text in template literals or JSX: the line has no\n * code-like characters before ` as ` — no `=`, `{`, `}`, `:`, `;`, `(`.\n */\nfunction isPlainText(trimmed: string): boolean {\n const idx = trimmed.indexOf(\" as \");\n if (idx < 0) return false;\n const before = trimmed.substring(0, idx);\n // If nothing before ` as ` looks like code, it's probably prose.\n return (\n !before.match(/[={}:;(]/) &&\n !before.match(/\\b(const|let|var|type|interface|function|return|await)\\b/)\n );\n}\n\n/**\n * Suggest a concrete fix for a specific violation pattern.\n */\n// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Pattern-matching advice is a linear chain of if-returns.\nexport function suggestFix(line: string): string | undefined {\n if (line.includes(\"JSON.parse\")) {\n return \"Use a validation function or type guard to parse and validate the result.\";\n }\n if (\n line.includes(\"as HTMLInputElement\") ||\n line.includes(\"as HTMLTextAreaElement\") ||\n line.includes(\"as HTMLButtonElement\")\n ) {\n return \"Use instanceof: if (el instanceof HTMLInputElement) { el.value ... }\";\n }\n if (line.includes(\"as HTMLElement\") || line.includes(\"as Element\")) {\n return \"Use instanceof: if (el instanceof HTMLElement) { ... }\";\n }\n if (line.includes(\".doc()\") && line.includes(\"as \")) {\n return \"Type the DocHandle generic: repo.find<MyType>(id) returns DocHandle<MyType>.\";\n }\n if (\n line.includes(\"Record<string, unknown>\") &&\n (line.includes(\"window\") || line.includes(\"globalThis\"))\n ) {\n return \"Extract a type guard: function getGlobalProp(name: string): unknown { ... }\";\n }\n if (line.includes(\"Record<string, unknown>\")) {\n return \"Use a type guard function that narrows the unknown value to the target shape.\";\n }\n if (line.includes(\"as PeerId\") || line.includes(\"as DocumentId\")) {\n return \"Use the library's branded-type constructor if available, or centralise the cast in a factory.\";\n }\n if (line.includes(\"as string\") || line.includes(\"as number\") || line.includes(\"as boolean\")) {\n return \"Narrow with typeof: if (typeof x === 'string') { ... }\";\n }\n if (line.includes(\"as any\")) {\n return \"Replace 'any' with 'unknown' and add a type guard or validation at the boundary.\";\n }\n return undefined;\n}\n\nfunction isFileExcluded(\n relative: string,\n excludeDirs: Set<string>,\n excludePackages: Set<string>,\n excludeFiles: Set<string>\n): boolean {\n const segments = relative.split(\"/\");\n if (segments.some((s) => excludeDirs.has(s))) return true;\n if (excludePackages.size > 0 && segments.some((s) => excludePackages.has(s))) return true;\n const basename = segments[segments.length - 1] ?? \"\";\n return excludeFiles.has(basename) || excludeFiles.has(relative);\n}\n\nfunction findViolations(relative: string, content: string): Violation[] {\n const results: Violation[] = [];\n const lines = content.split(\"\\n\");\n let insideTemplate = false;\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i] ?? \"\";\n const backticks = (line.match(/`/g) ?? []).length;\n const startedInTemplate = insideTemplate;\n if (backticks % 2 === 1) insideTemplate = !insideTemplate;\n\n // Line is entirely inside a multi-line template literal and has no\n // interpolation — treat as string content (e.g. SQL column aliases).\n if (startedInTemplate && backticks === 0 && !line.includes(\"${\")) continue;\n\n if (!isLineClean(line)) {\n results.push({\n file: relative,\n line: i + 1,\n content: line.trim(),\n advice: suggestFix(line.trim()),\n });\n }\n }\n return results;\n}\n\nfunction printViolations(violations: Violation[]): void {\n if (violations.length === 0) {\n console.log(\"[no-as-casting] ✅ No violations found.\");\n return;\n }\n console.log(`[no-as-casting] ❌ ${violations.length} violation(s) found:\\n`);\n for (const v of violations) {\n console.log(` ${v.file}:${v.line}`);\n console.log(` ${v.content}`);\n if (v.advice) console.log(` 💡 ${v.advice}`);\n console.log();\n }\n console.log(\"[no-as-casting] Use type guards, validation, or fix the types at the source.\");\n console.log('[no-as-casting] Only \"as const\" and \"as unknown as\" are allowed.');\n}\n\n/**\n * Run the no-as-casting check against a directory. Returns a result\n * object with the violations and a print function for CLI output.\n */\nexport async function checkNoAsCasting(options: CheckOptions): Promise<CheckResult> {\n const rootDir = options.rootDir;\n const excludeDirs = new Set(options.exclude ?? [\"node_modules\", \"dist\", \".git\", \".bun\"]);\n const excludePackages = new Set(options.excludePackages ?? []);\n const excludeFiles = new Set(options.excludeFiles ?? []);\n const pattern = options.filePatterns ?? \"**/*.{ts,tsx}\";\n const glob = new Glob(pattern);\n const violations: Violation[] = [];\n\n for await (const file of glob.scan({ cwd: rootDir, absolute: true })) {\n const relative = file.replace(`${rootDir}/`, \"\");\n if (isFileExcluded(relative, excludeDirs, excludePackages, excludeFiles)) continue;\n const content = readFileSync(file, \"utf-8\");\n violations.push(...findViolations(relative, content));\n }\n\n return { violations, print: () => printViolations(violations) };\n}\n",
|
|
6
|
+
"#!/usr/bin/env bun\n\n/**\n * CLI entry point for Polly quality checks.\n *\n * polly quality [--root <dir>] [--exclude <dirs>] [--pattern <glob>]\n * [--exclude-packages <names>] [--exclude-files <names>]\n *\n * Runs all conformance checks (currently: no-as-casting) against the\n * target directory and exits non-zero when violations are found.\n */\n\nimport { checkNoAsCasting } from \"./no-as-casting\";\n\nconst args = process.argv.slice(2);\n\nfunction getFlag(name: string): string | undefined {\n const idx = args.indexOf(`--${name}`);\n return idx >= 0 ? args[idx + 1] : undefined;\n}\n\nconst rootDir = getFlag(\"root\") ?? process.cwd();\nconst exclude = getFlag(\"exclude\")?.split(\",\") ?? [\"node_modules\", \"dist\", \".git\", \".bun\"];\nconst excludePackages = getFlag(\"exclude-packages\")?.split(\",\");\nconst excludeFiles = getFlag(\"exclude-files\")?.split(\",\");\nconst filePatterns = getFlag(\"pattern\");\n\nconst result = await checkNoAsCasting({\n rootDir,\n exclude,\n ...(excludePackages ? { excludePackages } : {}),\n ...(excludeFiles ? { excludeFiles } : {}),\n ...(filePatterns ? { filePatterns } : {}),\n});\n\nresult.print();\nprocess.exit(result.violations.length > 0 ? 1 : 0);\n"
|
|
7
7
|
],
|
|
8
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;AAcA;AACA;
|
|
9
|
-
"debugId": "
|
|
8
|
+
"mappings": "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",
|
|
9
|
+
"debugId": "D4F70D746137C24364756E2164756E21",
|
|
10
10
|
"names": []
|
|
11
11
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fairfox/polly/quality — conformance checks for Polly applications.
|
|
3
|
+
*
|
|
4
|
+
* Exports the same quality rules that Polly enforces on itself, so
|
|
5
|
+
* consuming applications can adopt the same standards. The flagship
|
|
6
|
+
* check is `isLineClean` which detects forbidden `as` type assertions;
|
|
7
|
+
* applications wire it into their own CI or pre-commit hook via the
|
|
8
|
+
* companion `checkNoAsCasting` runner.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* import { checkNoAsCasting } from "@fairfox/polly/quality";
|
|
13
|
+
*
|
|
14
|
+
* const result = await checkNoAsCasting({
|
|
15
|
+
* rootDir: process.cwd(),
|
|
16
|
+
* exclude: ["node_modules", "dist"],
|
|
17
|
+
* });
|
|
18
|
+
*
|
|
19
|
+
* if (result.violations.length > 0) {
|
|
20
|
+
* result.print();
|
|
21
|
+
* process.exit(1);
|
|
22
|
+
* }
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export { type CheckResult, checkNoAsCasting, isLineClean, suggestFix, type Violation, } from "./no-as-casting";
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __returnValue = (v) => v;
|
|
4
|
+
function __exportSetter(name, newValue) {
|
|
5
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
6
|
+
}
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, {
|
|
10
|
+
get: all[name],
|
|
11
|
+
enumerable: true,
|
|
12
|
+
configurable: true,
|
|
13
|
+
set: __exportSetter.bind(all, name)
|
|
14
|
+
});
|
|
15
|
+
};
|
|
16
|
+
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
17
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
18
|
+
|
|
19
|
+
// tools/quality/src/no-as-casting.ts
|
|
20
|
+
import { readFileSync } from "node:fs";
|
|
21
|
+
import { Glob } from "bun";
|
|
22
|
+
function isLineClean(line) {
|
|
23
|
+
if (!line.includes(" as "))
|
|
24
|
+
return true;
|
|
25
|
+
const trimmed = line.trim();
|
|
26
|
+
if (trimmed.startsWith("//") || trimmed.startsWith("*") || trimmed.startsWith("/*")) {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
if (line.match(/\bas const\b/)) {
|
|
30
|
+
const withoutAsConst = line.replace(/\bas const\b/g, "");
|
|
31
|
+
if (!withoutAsConst.includes(" as "))
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
if (line.includes(" as unknown as ") || line.trimEnd().endsWith("as unknown as")) {
|
|
35
|
+
const withoutEscapeHatch = line.replace(/\bas unknown as\b/g, "");
|
|
36
|
+
if (!withoutEscapeHatch.includes(" as "))
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
if (line.match(/\b(import|export)\s+.*\s+as\s+\w+/) || line.match(/\b(import|export)\s+\*\s+as\s+\w+/) || line.match(/\b(import|export)\s+type\s+.*\s+as\s+\w+/) || line.match(/^\s*\w+\s+as\s+\w+,?\s*$/) || line.match(/^\s*type\s+\w+\s+as\s+\w+,?\s*$/)) {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
if (line.match(/\bas\s*[=:,]/))
|
|
43
|
+
return true;
|
|
44
|
+
if (everyAsInsideString(line))
|
|
45
|
+
return true;
|
|
46
|
+
if (isJsxText(trimmed))
|
|
47
|
+
return true;
|
|
48
|
+
if (isPlainText(trimmed))
|
|
49
|
+
return true;
|
|
50
|
+
const commentIdx = line.indexOf("//");
|
|
51
|
+
if (commentIdx >= 0 && line.indexOf(" as ", commentIdx) >= 0) {
|
|
52
|
+
const beforeComment = line.substring(0, commentIdx);
|
|
53
|
+
if (!beforeComment.includes(" as "))
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
if (line.match(/"\)\s+as\s+\w+"/))
|
|
57
|
+
return true;
|
|
58
|
+
if (line.includes(" satisfies "))
|
|
59
|
+
return true;
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
function everyAsInsideString(line) {
|
|
63
|
+
let searchFrom = 0;
|
|
64
|
+
while (true) {
|
|
65
|
+
const idx = line.indexOf(" as ", searchFrom);
|
|
66
|
+
if (idx < 0)
|
|
67
|
+
return true;
|
|
68
|
+
const before = line.substring(0, idx);
|
|
69
|
+
const singleQuotes = (before.match(/'/g) ?? []).length;
|
|
70
|
+
const doubleQuotes = (before.match(/"/g) ?? []).length;
|
|
71
|
+
const backticks = (before.match(/`/g) ?? []).length;
|
|
72
|
+
if (singleQuotes % 2 === 0 && doubleQuotes % 2 === 0 && backticks % 2 === 0) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
searchFrom = idx + 4;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function isJsxText(trimmed) {
|
|
79
|
+
if (trimmed.match(/^[^{};=()]*\bas\b[^{};=()]*$/)) {
|
|
80
|
+
if (!trimmed.match(/\bas\s+[A-Z]\w*/) && !trimmed.match(/\bas\s+(string|number|boolean|any|unknown|never)\b/)) {
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
function isPlainText(trimmed) {
|
|
87
|
+
const idx = trimmed.indexOf(" as ");
|
|
88
|
+
if (idx < 0)
|
|
89
|
+
return false;
|
|
90
|
+
const before = trimmed.substring(0, idx);
|
|
91
|
+
return !before.match(/[={}:;(]/) && !before.match(/\b(const|let|var|type|interface|function|return|await)\b/);
|
|
92
|
+
}
|
|
93
|
+
function suggestFix(line) {
|
|
94
|
+
if (line.includes("JSON.parse")) {
|
|
95
|
+
return "Use a validation function or type guard to parse and validate the result.";
|
|
96
|
+
}
|
|
97
|
+
if (line.includes("as HTMLInputElement") || line.includes("as HTMLTextAreaElement") || line.includes("as HTMLButtonElement")) {
|
|
98
|
+
return "Use instanceof: if (el instanceof HTMLInputElement) { el.value ... }";
|
|
99
|
+
}
|
|
100
|
+
if (line.includes("as HTMLElement") || line.includes("as Element")) {
|
|
101
|
+
return "Use instanceof: if (el instanceof HTMLElement) { ... }";
|
|
102
|
+
}
|
|
103
|
+
if (line.includes(".doc()") && line.includes("as ")) {
|
|
104
|
+
return "Type the DocHandle generic: repo.find<MyType>(id) returns DocHandle<MyType>.";
|
|
105
|
+
}
|
|
106
|
+
if (line.includes("Record<string, unknown>") && (line.includes("window") || line.includes("globalThis"))) {
|
|
107
|
+
return "Extract a type guard: function getGlobalProp(name: string): unknown { ... }";
|
|
108
|
+
}
|
|
109
|
+
if (line.includes("Record<string, unknown>")) {
|
|
110
|
+
return "Use a type guard function that narrows the unknown value to the target shape.";
|
|
111
|
+
}
|
|
112
|
+
if (line.includes("as PeerId") || line.includes("as DocumentId")) {
|
|
113
|
+
return "Use the library's branded-type constructor if available, or centralise the cast in a factory.";
|
|
114
|
+
}
|
|
115
|
+
if (line.includes("as string") || line.includes("as number") || line.includes("as boolean")) {
|
|
116
|
+
return "Narrow with typeof: if (typeof x === 'string') { ... }";
|
|
117
|
+
}
|
|
118
|
+
if (line.includes("as any")) {
|
|
119
|
+
return "Replace 'any' with 'unknown' and add a type guard or validation at the boundary.";
|
|
120
|
+
}
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
function isFileExcluded(relative, excludeDirs, excludePackages, excludeFiles) {
|
|
124
|
+
const segments = relative.split("/");
|
|
125
|
+
if (segments.some((s) => excludeDirs.has(s)))
|
|
126
|
+
return true;
|
|
127
|
+
if (excludePackages.size > 0 && segments.some((s) => excludePackages.has(s)))
|
|
128
|
+
return true;
|
|
129
|
+
const basename = segments[segments.length - 1] ?? "";
|
|
130
|
+
return excludeFiles.has(basename) || excludeFiles.has(relative);
|
|
131
|
+
}
|
|
132
|
+
function findViolations(relative, content) {
|
|
133
|
+
const results = [];
|
|
134
|
+
const lines = content.split(`
|
|
135
|
+
`);
|
|
136
|
+
let insideTemplate = false;
|
|
137
|
+
for (let i = 0;i < lines.length; i++) {
|
|
138
|
+
const line = lines[i] ?? "";
|
|
139
|
+
const backticks = (line.match(/`/g) ?? []).length;
|
|
140
|
+
const startedInTemplate = insideTemplate;
|
|
141
|
+
if (backticks % 2 === 1)
|
|
142
|
+
insideTemplate = !insideTemplate;
|
|
143
|
+
if (startedInTemplate && backticks === 0 && !line.includes("${"))
|
|
144
|
+
continue;
|
|
145
|
+
if (!isLineClean(line)) {
|
|
146
|
+
results.push({
|
|
147
|
+
file: relative,
|
|
148
|
+
line: i + 1,
|
|
149
|
+
content: line.trim(),
|
|
150
|
+
advice: suggestFix(line.trim())
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return results;
|
|
155
|
+
}
|
|
156
|
+
function printViolations(violations) {
|
|
157
|
+
if (violations.length === 0) {
|
|
158
|
+
console.log("[no-as-casting] ✅ No violations found.");
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
console.log(`[no-as-casting] ❌ ${violations.length} violation(s) found:
|
|
162
|
+
`);
|
|
163
|
+
for (const v of violations) {
|
|
164
|
+
console.log(` ${v.file}:${v.line}`);
|
|
165
|
+
console.log(` ${v.content}`);
|
|
166
|
+
if (v.advice)
|
|
167
|
+
console.log(` \uD83D\uDCA1 ${v.advice}`);
|
|
168
|
+
console.log();
|
|
169
|
+
}
|
|
170
|
+
console.log("[no-as-casting] Use type guards, validation, or fix the types at the source.");
|
|
171
|
+
console.log('[no-as-casting] Only "as const" and "as unknown as" are allowed.');
|
|
172
|
+
}
|
|
173
|
+
async function checkNoAsCasting(options) {
|
|
174
|
+
const rootDir = options.rootDir;
|
|
175
|
+
const excludeDirs = new Set(options.exclude ?? ["node_modules", "dist", ".git", ".bun"]);
|
|
176
|
+
const excludePackages = new Set(options.excludePackages ?? []);
|
|
177
|
+
const excludeFiles = new Set(options.excludeFiles ?? []);
|
|
178
|
+
const pattern = options.filePatterns ?? "**/*.{ts,tsx}";
|
|
179
|
+
const glob = new Glob(pattern);
|
|
180
|
+
const violations = [];
|
|
181
|
+
for await (const file of glob.scan({ cwd: rootDir, absolute: true })) {
|
|
182
|
+
const relative = file.replace(`${rootDir}/`, "");
|
|
183
|
+
if (isFileExcluded(relative, excludeDirs, excludePackages, excludeFiles))
|
|
184
|
+
continue;
|
|
185
|
+
const content = readFileSync(file, "utf-8");
|
|
186
|
+
violations.push(...findViolations(relative, content));
|
|
187
|
+
}
|
|
188
|
+
return { violations, print: () => printViolations(violations) };
|
|
189
|
+
}
|
|
190
|
+
export {
|
|
191
|
+
suggestFix,
|
|
192
|
+
isLineClean,
|
|
193
|
+
checkNoAsCasting
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
//# debugId=8394B2630EA383EC64756E2164756E21
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../tools/quality/src/no-as-casting.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * No-as-casting conformance check.\n *\n * Bans all TypeScript type assertions (`as Type`) except the allowed\n * patterns: `as const` (literal narrowing), `as unknown as` (explicit\n * escape hatch), import/export renames, and `as` inside strings or\n * comments. Violations include pattern-specific fix advice.\n *\n * This module exports the check logic as a library so consuming\n * applications can import it from `@fairfox/polly/quality` and run it\n * programmatically. Polly's own `scripts/check-no-as-casting.ts` is a\n * thin CLI wrapper around these exports.\n */\n\nimport { readFileSync } from \"node:fs\";\nimport { Glob } from \"bun\";\n\nexport interface Violation {\n file: string;\n line: number;\n content: string;\n advice?: string;\n}\n\nexport interface CheckResult {\n violations: Violation[];\n print: () => void;\n}\n\nexport interface CheckOptions {\n rootDir: string;\n exclude?: string[];\n excludePackages?: string[];\n excludeFiles?: string[];\n filePatterns?: string;\n}\n\n/**\n * Check whether a line contains a forbidden `as` type assertion.\n * Returns true if the line is clean (no violation), false if it violates.\n */\n// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Source-text scanning with many skip rules is inherently branchy.\nexport function isLineClean(line: string): boolean {\n if (!line.includes(\" as \")) return true;\n\n const trimmed = line.trim();\n\n // Full-line comments\n if (trimmed.startsWith(\"//\") || trimmed.startsWith(\"*\") || trimmed.startsWith(\"/*\")) {\n return true;\n }\n\n // as const (literal narrowing)\n if (line.match(/\\bas const\\b/)) {\n const withoutAsConst = line.replace(/\\bas const\\b/g, \"\");\n if (!withoutAsConst.includes(\" as \")) return true;\n }\n\n // as unknown as (explicit escape hatch)\n if (line.includes(\" as unknown as \") || line.trimEnd().endsWith(\"as unknown as\")) {\n const withoutEscapeHatch = line.replace(/\\bas unknown as\\b/g, \"\");\n if (!withoutEscapeHatch.includes(\" as \")) return true;\n }\n\n // Import/export renames\n if (\n line.match(/\\b(import|export)\\s+.*\\s+as\\s+\\w+/) ||\n line.match(/\\b(import|export)\\s+\\*\\s+as\\s+\\w+/) ||\n line.match(/\\b(import|export)\\s+type\\s+.*\\s+as\\s+\\w+/) ||\n line.match(/^\\s*\\w+\\s+as\\s+\\w+,?\\s*$/) ||\n line.match(/^\\s*type\\s+\\w+\\s+as\\s+\\w+,?\\s*$/)\n ) {\n return true;\n }\n\n // Property declarations: as= or as: or as,\n if (line.match(/\\bas\\s*[=:,]/)) return true;\n\n // String literal detection: count quotes before each ` as ` occurrence.\n // If any quote type has an odd count, the ` as ` is inside a string.\n if (everyAsInsideString(line)) return true;\n\n // JSX text: ` as ` between > and < with no code syntax around it\n if (isJsxText(trimmed)) return true;\n\n // Plain text heuristic: indented line with no code syntax characters\n // before ` as ` — catches multiline JSX text and template literal bodies.\n if (isPlainText(trimmed)) return true;\n\n // Inline comment: ` as ` appears only after //\n const commentIdx = line.indexOf(\"//\");\n if (commentIdx >= 0 && line.indexOf(\" as \", commentIdx) >= 0) {\n const beforeComment = line.substring(0, commentIdx);\n if (!beforeComment.includes(\" as \")) return true;\n }\n\n // SQL alias: `) as column_name`\n if (line.match(/\"\\)\\s+as\\s+\\w+\"/)) return true;\n\n if (line.includes(\" satisfies \")) return true;\n\n return false;\n}\n\n/**\n * Returns true when every ` as ` occurrence in the line falls inside a\n * string literal (single-quoted, double-quoted, or backtick).\n */\nfunction everyAsInsideString(line: string): boolean {\n let searchFrom = 0;\n while (true) {\n const idx = line.indexOf(\" as \", searchFrom);\n if (idx < 0) return true; // no more ` as ` to check\n const before = line.substring(0, idx);\n const singleQuotes = (before.match(/'/g) ?? []).length;\n const doubleQuotes = (before.match(/\"/g) ?? []).length;\n const backticks = (before.match(/`/g) ?? []).length;\n if (singleQuotes % 2 === 0 && doubleQuotes % 2 === 0 && backticks % 2 === 0) {\n return false; // this ` as ` is outside any string\n }\n searchFrom = idx + 4;\n }\n}\n\n/**\n * Detects JSX text content — ` as ` appearing between > and < with no\n * code-like syntax around it (no braces, semicolons, equals signs).\n */\nfunction isJsxText(trimmed: string): boolean {\n // Classic JSX text: starts after > or is plain text ending before <\n if (trimmed.match(/^[^{};=()]*\\bas\\b[^{};=()]*$/)) {\n // No code syntax at all — could be JSX text or template literal body.\n // Reject if it looks like a type assertion (word ` as ` TypeName pattern\n // where TypeName starts with uppercase, or is a known TS type).\n if (\n !trimmed.match(/\\bas\\s+[A-Z]\\w*/) &&\n !trimmed.match(/\\bas\\s+(string|number|boolean|any|unknown|never)\\b/)\n ) {\n return true;\n }\n }\n return false;\n}\n\n/**\n * Heuristic for plain text in template literals or JSX: the line has no\n * code-like characters before ` as ` — no `=`, `{`, `}`, `:`, `;`, `(`.\n */\nfunction isPlainText(trimmed: string): boolean {\n const idx = trimmed.indexOf(\" as \");\n if (idx < 0) return false;\n const before = trimmed.substring(0, idx);\n // If nothing before ` as ` looks like code, it's probably prose.\n return (\n !before.match(/[={}:;(]/) &&\n !before.match(/\\b(const|let|var|type|interface|function|return|await)\\b/)\n );\n}\n\n/**\n * Suggest a concrete fix for a specific violation pattern.\n */\n// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Pattern-matching advice is a linear chain of if-returns.\nexport function suggestFix(line: string): string | undefined {\n if (line.includes(\"JSON.parse\")) {\n return \"Use a validation function or type guard to parse and validate the result.\";\n }\n if (\n line.includes(\"as HTMLInputElement\") ||\n line.includes(\"as HTMLTextAreaElement\") ||\n line.includes(\"as HTMLButtonElement\")\n ) {\n return \"Use instanceof: if (el instanceof HTMLInputElement) { el.value ... }\";\n }\n if (line.includes(\"as HTMLElement\") || line.includes(\"as Element\")) {\n return \"Use instanceof: if (el instanceof HTMLElement) { ... }\";\n }\n if (line.includes(\".doc()\") && line.includes(\"as \")) {\n return \"Type the DocHandle generic: repo.find<MyType>(id) returns DocHandle<MyType>.\";\n }\n if (\n line.includes(\"Record<string, unknown>\") &&\n (line.includes(\"window\") || line.includes(\"globalThis\"))\n ) {\n return \"Extract a type guard: function getGlobalProp(name: string): unknown { ... }\";\n }\n if (line.includes(\"Record<string, unknown>\")) {\n return \"Use a type guard function that narrows the unknown value to the target shape.\";\n }\n if (line.includes(\"as PeerId\") || line.includes(\"as DocumentId\")) {\n return \"Use the library's branded-type constructor if available, or centralise the cast in a factory.\";\n }\n if (line.includes(\"as string\") || line.includes(\"as number\") || line.includes(\"as boolean\")) {\n return \"Narrow with typeof: if (typeof x === 'string') { ... }\";\n }\n if (line.includes(\"as any\")) {\n return \"Replace 'any' with 'unknown' and add a type guard or validation at the boundary.\";\n }\n return undefined;\n}\n\nfunction isFileExcluded(\n relative: string,\n excludeDirs: Set<string>,\n excludePackages: Set<string>,\n excludeFiles: Set<string>\n): boolean {\n const segments = relative.split(\"/\");\n if (segments.some((s) => excludeDirs.has(s))) return true;\n if (excludePackages.size > 0 && segments.some((s) => excludePackages.has(s))) return true;\n const basename = segments[segments.length - 1] ?? \"\";\n return excludeFiles.has(basename) || excludeFiles.has(relative);\n}\n\nfunction findViolations(relative: string, content: string): Violation[] {\n const results: Violation[] = [];\n const lines = content.split(\"\\n\");\n let insideTemplate = false;\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i] ?? \"\";\n const backticks = (line.match(/`/g) ?? []).length;\n const startedInTemplate = insideTemplate;\n if (backticks % 2 === 1) insideTemplate = !insideTemplate;\n\n // Line is entirely inside a multi-line template literal and has no\n // interpolation — treat as string content (e.g. SQL column aliases).\n if (startedInTemplate && backticks === 0 && !line.includes(\"${\")) continue;\n\n if (!isLineClean(line)) {\n results.push({\n file: relative,\n line: i + 1,\n content: line.trim(),\n advice: suggestFix(line.trim()),\n });\n }\n }\n return results;\n}\n\nfunction printViolations(violations: Violation[]): void {\n if (violations.length === 0) {\n console.log(\"[no-as-casting] ✅ No violations found.\");\n return;\n }\n console.log(`[no-as-casting] ❌ ${violations.length} violation(s) found:\\n`);\n for (const v of violations) {\n console.log(` ${v.file}:${v.line}`);\n console.log(` ${v.content}`);\n if (v.advice) console.log(` 💡 ${v.advice}`);\n console.log();\n }\n console.log(\"[no-as-casting] Use type guards, validation, or fix the types at the source.\");\n console.log('[no-as-casting] Only \"as const\" and \"as unknown as\" are allowed.');\n}\n\n/**\n * Run the no-as-casting check against a directory. Returns a result\n * object with the violations and a print function for CLI output.\n */\nexport async function checkNoAsCasting(options: CheckOptions): Promise<CheckResult> {\n const rootDir = options.rootDir;\n const excludeDirs = new Set(options.exclude ?? [\"node_modules\", \"dist\", \".git\", \".bun\"]);\n const excludePackages = new Set(options.excludePackages ?? []);\n const excludeFiles = new Set(options.excludeFiles ?? []);\n const pattern = options.filePatterns ?? \"**/*.{ts,tsx}\";\n const glob = new Glob(pattern);\n const violations: Violation[] = [];\n\n for await (const file of glob.scan({ cwd: rootDir, absolute: true })) {\n const relative = file.replace(`${rootDir}/`, \"\");\n if (isFileExcluded(relative, excludeDirs, excludePackages, excludeFiles)) continue;\n const content = readFileSync(file, \"utf-8\");\n violations.push(...findViolations(relative, content));\n }\n\n return { violations, print: () => printViolations(violations) };\n}\n"
|
|
6
|
+
],
|
|
7
|
+
"mappings": "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",
|
|
8
|
+
"debugId": "8394B2630EA383EC64756E2164756E21",
|
|
9
|
+
"names": []
|
|
10
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* No-as-casting conformance check.
|
|
3
|
+
*
|
|
4
|
+
* Bans all TypeScript type assertions (`as Type`) except the allowed
|
|
5
|
+
* patterns: `as const` (literal narrowing), `as unknown as` (explicit
|
|
6
|
+
* escape hatch), import/export renames, and `as` inside strings or
|
|
7
|
+
* comments. Violations include pattern-specific fix advice.
|
|
8
|
+
*
|
|
9
|
+
* This module exports the check logic as a library so consuming
|
|
10
|
+
* applications can import it from `@fairfox/polly/quality` and run it
|
|
11
|
+
* programmatically. Polly's own `scripts/check-no-as-casting.ts` is a
|
|
12
|
+
* thin CLI wrapper around these exports.
|
|
13
|
+
*/
|
|
14
|
+
export interface Violation {
|
|
15
|
+
file: string;
|
|
16
|
+
line: number;
|
|
17
|
+
content: string;
|
|
18
|
+
advice?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface CheckResult {
|
|
21
|
+
violations: Violation[];
|
|
22
|
+
print: () => void;
|
|
23
|
+
}
|
|
24
|
+
export interface CheckOptions {
|
|
25
|
+
rootDir: string;
|
|
26
|
+
exclude?: string[];
|
|
27
|
+
excludePackages?: string[];
|
|
28
|
+
excludeFiles?: string[];
|
|
29
|
+
filePatterns?: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Check whether a line contains a forbidden `as` type assertion.
|
|
33
|
+
* Returns true if the line is clean (no violation), false if it violates.
|
|
34
|
+
*/
|
|
35
|
+
export declare function isLineClean(line: string): boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Suggest a concrete fix for a specific violation pattern.
|
|
38
|
+
*/
|
|
39
|
+
export declare function suggestFix(line: string): string | undefined;
|
|
40
|
+
/**
|
|
41
|
+
* Run the no-as-casting check against a directory. Returns a result
|
|
42
|
+
* object with the violations and a print function for CLI output.
|
|
43
|
+
*/
|
|
44
|
+
export declare function checkNoAsCasting(options: CheckOptions): Promise<CheckResult>;
|