@konvert7/klint 0.16.0 → 0.18.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/cli.ts
CHANGED
|
@@ -18,6 +18,13 @@ import type { ArchConfig, KlintConfig, KlintRule, RuleConfigValue } from "./core
|
|
|
18
18
|
import { BUILT_IN_PLUGINS } from "./plugins/index";
|
|
19
19
|
import { BUILT_IN_RULES } from "./rules/index";
|
|
20
20
|
|
|
21
|
+
const RUST_SUPPORTED_TYPESCRIPT_RULES = new Set([
|
|
22
|
+
"no-consecutive-array-push",
|
|
23
|
+
"no-nested-template-literals",
|
|
24
|
+
"no-string-match",
|
|
25
|
+
"no-unguarded-json-parse",
|
|
26
|
+
]);
|
|
27
|
+
|
|
21
28
|
interface CliOptions {
|
|
22
29
|
configDir?: string;
|
|
23
30
|
rulesFile?: string;
|
|
@@ -42,10 +49,12 @@ export async function main(opts: CliOptions = {}): Promise<void> {
|
|
|
42
49
|
let fix = false;
|
|
43
50
|
let json = false;
|
|
44
51
|
let debug = false;
|
|
52
|
+
let engine = process.env.KLINT_ENGINE;
|
|
45
53
|
|
|
46
54
|
for (let i = 0; i < args.length; i++) {
|
|
47
55
|
if (args[i] === "--config" && args[i + 1]) configDir = resolve(args[++i]);
|
|
48
56
|
else if (args[i] === "--rules" && args[i + 1]) rulesFile = resolve(args[++i]);
|
|
57
|
+
else if (args[i] === "--engine" && args[i + 1]) engine = args[++i];
|
|
49
58
|
else if (args[i] === "--fix") fix = true;
|
|
50
59
|
else if (args[i] === "--json") json = true;
|
|
51
60
|
else if (args[i] === "--debug" || args[i] === "-debug") debug = true;
|
|
@@ -85,12 +94,17 @@ export async function main(opts: CliOptions = {}): Promise<void> {
|
|
|
85
94
|
process.exit(1);
|
|
86
95
|
}
|
|
87
96
|
const root = resolve(configDir, raw.root ?? ".");
|
|
88
|
-
const engine = process.env.KLINT_ENGINE;
|
|
89
97
|
|
|
90
98
|
if (engine === "rust") {
|
|
91
99
|
runRustEngine({ configDir, fix, json, raw, rulesFile });
|
|
92
100
|
return;
|
|
93
101
|
}
|
|
102
|
+
if (engine !== undefined && engine !== "ts" && engine !== "compare") {
|
|
103
|
+
process.stderr.write(
|
|
104
|
+
`klint: unknown engine "${engine}" (expected "ts", "rust", or "compare")\n`
|
|
105
|
+
);
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
94
108
|
|
|
95
109
|
let customRules: Record<string, KlintRule> = {};
|
|
96
110
|
const defaultRulesPath = resolve(configDir, "klint.rules.ts");
|
|
@@ -118,15 +132,15 @@ export async function main(opts: CliOptions = {}): Promise<void> {
|
|
|
118
132
|
{ onDebug: debug ? writeDebugEvent : undefined }
|
|
119
133
|
);
|
|
120
134
|
|
|
135
|
+
if (engine === "compare") {
|
|
136
|
+
runCompareEngine({ configDir, fix, json, raw, rulesFile, tsViolations: violations });
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
121
140
|
if (json) {
|
|
122
|
-
const
|
|
123
|
-
process.stdout.write(
|
|
124
|
-
|
|
125
|
-
violations: violations.map((v) => ({ ...v, fix: v.fix ?? null })),
|
|
126
|
-
summary: { errors: errors.length, warnings: violations.length - errors.length },
|
|
127
|
-
})
|
|
128
|
-
);
|
|
129
|
-
process.exit(errors.length > 0 ? 2 : 0);
|
|
141
|
+
const tsResult = toJsonCliResult(violations);
|
|
142
|
+
process.stdout.write(tsResult.stdout);
|
|
143
|
+
process.exit(tsResult.status);
|
|
130
144
|
}
|
|
131
145
|
|
|
132
146
|
if (fix) {
|
|
@@ -240,6 +254,83 @@ function runRustEngine({
|
|
|
240
254
|
process.exit(result.status ?? 1);
|
|
241
255
|
}
|
|
242
256
|
|
|
257
|
+
function runCompareEngine({
|
|
258
|
+
configDir,
|
|
259
|
+
fix,
|
|
260
|
+
json,
|
|
261
|
+
raw,
|
|
262
|
+
rulesFile,
|
|
263
|
+
tsViolations,
|
|
264
|
+
}: {
|
|
265
|
+
configDir: string;
|
|
266
|
+
fix: boolean;
|
|
267
|
+
json: boolean;
|
|
268
|
+
raw: {
|
|
269
|
+
plugins?: string[];
|
|
270
|
+
rules?: Record<string, RuleConfigValue>;
|
|
271
|
+
arch?: unknown;
|
|
272
|
+
};
|
|
273
|
+
rulesFile?: string;
|
|
274
|
+
tsViolations: ReturnType<typeof runKlint>;
|
|
275
|
+
}): void {
|
|
276
|
+
const unsupportedReason = rustEngineUnsupportedReason({
|
|
277
|
+
fix,
|
|
278
|
+
json,
|
|
279
|
+
raw,
|
|
280
|
+
rulesFile,
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
if (unsupportedReason) {
|
|
284
|
+
process.stderr.write(`klint: ${unsupportedReason}\n`);
|
|
285
|
+
process.exit(1);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const tsResult = toJsonCliResult(tsViolations);
|
|
289
|
+
const rustCommand = resolveRustEngineCommand(configDir);
|
|
290
|
+
const rustResult = spawnSync(rustCommand.bin, rustCommand.args, {
|
|
291
|
+
cwd: import.meta.dir,
|
|
292
|
+
encoding: "utf-8",
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
const rustStatus = rustResult.status ?? 1;
|
|
296
|
+
if (rustResult.stderr) process.stderr.write(rustResult.stderr);
|
|
297
|
+
|
|
298
|
+
const tsPayload = parseJsonPayload(tsResult.stdout);
|
|
299
|
+
const rustPayload = parseJsonPayload(rustResult.stdout);
|
|
300
|
+
|
|
301
|
+
if (
|
|
302
|
+
rustStatus !== tsResult.status ||
|
|
303
|
+
tsPayload === undefined ||
|
|
304
|
+
rustPayload === undefined ||
|
|
305
|
+
!jsonPayloadEquals(tsPayload, rustPayload)
|
|
306
|
+
) {
|
|
307
|
+
const mismatchLines = [
|
|
308
|
+
"klint: compare engine mismatch",
|
|
309
|
+
`TypeScript exit: ${tsResult.status}`,
|
|
310
|
+
`Rust exit: ${rustStatus}`,
|
|
311
|
+
];
|
|
312
|
+
process.stderr.write(`${mismatchLines.join("\n")}\n`);
|
|
313
|
+
process.exit(1);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
process.stdout.write(tsResult.stdout);
|
|
317
|
+
process.exit(tsResult.status);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function toJsonCliResult(violations: ReturnType<typeof runKlint>): {
|
|
321
|
+
stdout: string;
|
|
322
|
+
status: number;
|
|
323
|
+
} {
|
|
324
|
+
const errors = violations.filter((v) => v.severity === "error");
|
|
325
|
+
return {
|
|
326
|
+
stdout: JSON.stringify({
|
|
327
|
+
violations: violations.map((v) => ({ ...v, fix: v.fix ?? null })),
|
|
328
|
+
summary: { errors: errors.length, warnings: violations.length - errors.length },
|
|
329
|
+
}),
|
|
330
|
+
status: errors.length > 0 ? 2 : 0,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
243
334
|
function writeDebugEvent(event: KlintDebugEvent): void {
|
|
244
335
|
switch (event.type) {
|
|
245
336
|
case "walk:start":
|
|
@@ -282,27 +373,31 @@ function resolveRustEngineCommand(configDir: string): RustEngineCommand {
|
|
|
282
373
|
return { bin: explicitBin, args };
|
|
283
374
|
}
|
|
284
375
|
|
|
285
|
-
const nativePackageBin = resolveNativePackageBinary({ packageRoot: import.meta.dir });
|
|
286
|
-
if (nativePackageBin) {
|
|
287
|
-
return { bin: nativePackageBin, args };
|
|
288
|
-
}
|
|
289
|
-
|
|
290
376
|
const localBin = join(
|
|
291
377
|
import.meta.dir,
|
|
292
378
|
"target",
|
|
293
379
|
"debug",
|
|
294
380
|
process.platform === "win32" ? "klint-rs.exe" : "klint-rs"
|
|
295
381
|
);
|
|
296
|
-
if (existsSync(localBin)) {
|
|
382
|
+
if (isSourceCheckout() && existsSync(localBin)) {
|
|
297
383
|
return { bin: localBin, args };
|
|
298
384
|
}
|
|
299
385
|
|
|
386
|
+
const nativePackageBin = resolveNativePackageBinary({ packageRoot: import.meta.dir });
|
|
387
|
+
if (nativePackageBin) {
|
|
388
|
+
return { bin: nativePackageBin, args };
|
|
389
|
+
}
|
|
390
|
+
|
|
300
391
|
return {
|
|
301
392
|
bin: "cargo",
|
|
302
393
|
args: ["run", "--quiet", "-p", "klint-rs", "--", ...args],
|
|
303
394
|
};
|
|
304
395
|
}
|
|
305
396
|
|
|
397
|
+
function isSourceCheckout(): boolean {
|
|
398
|
+
return existsSync(join(import.meta.dir, "crates", "klint-rs", "Cargo.toml"));
|
|
399
|
+
}
|
|
400
|
+
|
|
306
401
|
function rustEngineUnsupportedReason({
|
|
307
402
|
fix,
|
|
308
403
|
json,
|
|
@@ -321,14 +416,58 @@ function rustEngineUnsupportedReason({
|
|
|
321
416
|
if (!json) return "KLINT_ENGINE=rust currently requires --json";
|
|
322
417
|
if (fix) return "KLINT_ENGINE=rust does not support --fix";
|
|
323
418
|
if (rulesFile) return "KLINT_ENGINE=rust does not support --rules";
|
|
324
|
-
if (!raw.arch) return "KLINT_ENGINE=rust requires an arch config";
|
|
325
419
|
if ((raw.plugins?.length ?? 0) > 0) return "KLINT_ENGINE=rust does not support plugins";
|
|
326
|
-
|
|
327
|
-
|
|
420
|
+
const activeSupportedRules = Object.entries(raw.rules ?? {}).filter(
|
|
421
|
+
([name, value]) =>
|
|
422
|
+
ruleConfigSeverity(value) !== "off" && RUST_SUPPORTED_TYPESCRIPT_RULES.has(name)
|
|
423
|
+
);
|
|
424
|
+
const unsupportedRules = Object.entries(raw.rules ?? {})
|
|
425
|
+
.filter(
|
|
426
|
+
([name, value]) =>
|
|
427
|
+
ruleConfigSeverity(value) !== "off" && !RUST_SUPPORTED_TYPESCRIPT_RULES.has(name)
|
|
428
|
+
)
|
|
429
|
+
.map(([name]) => name);
|
|
430
|
+
if (!raw.arch && activeSupportedRules.length === 0 && unsupportedRules.length === 0) {
|
|
431
|
+
return "KLINT_ENGINE=rust requires an arch config or supported TypeScript rule";
|
|
432
|
+
}
|
|
433
|
+
if (unsupportedRules.length > 0) {
|
|
434
|
+
return [
|
|
435
|
+
"Rust engine currently supports arch rules and selected TypeScript rules only",
|
|
436
|
+
"",
|
|
437
|
+
"Unsupported TypeScript rules:",
|
|
438
|
+
...unsupportedRules.map((rule) => `- ${rule}`),
|
|
439
|
+
].join("\n");
|
|
328
440
|
}
|
|
329
441
|
return undefined;
|
|
330
442
|
}
|
|
331
443
|
|
|
444
|
+
function ruleConfigSeverity(value: RuleConfigValue): string | undefined {
|
|
445
|
+
if (typeof value === "string") return value;
|
|
446
|
+
return value.severity;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function parseJsonPayload(value: string): unknown {
|
|
450
|
+
try {
|
|
451
|
+
return JSON.parse(value);
|
|
452
|
+
} catch {
|
|
453
|
+
return undefined;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function jsonPayloadEquals(a: unknown, b: unknown): boolean {
|
|
458
|
+
return JSON.stringify(canonicalJson(a)) === JSON.stringify(canonicalJson(b));
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function canonicalJson(value: unknown): unknown {
|
|
462
|
+
if (Array.isArray(value)) return value.map(canonicalJson);
|
|
463
|
+
if (!value || typeof value !== "object") return value;
|
|
464
|
+
return Object.fromEntries(
|
|
465
|
+
Object.entries(value)
|
|
466
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
467
|
+
.map(([key, entry]) => [key, canonicalJson(entry)])
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
|
|
332
471
|
const AGENT_TARGETS = [
|
|
333
472
|
{ value: "claude", label: "Claude Code" },
|
|
334
473
|
{ value: "opencode", label: "opencode" },
|
|
@@ -437,11 +576,12 @@ function printHelp(): void {
|
|
|
437
576
|
[
|
|
438
577
|
"klint — agent harness for TypeScript architecture rules",
|
|
439
578
|
"",
|
|
440
|
-
"Usage: klint [--config <dir>] [--rules <file>] [--fix] [--json]",
|
|
579
|
+
"Usage: klint [--config <dir>] [--rules <file>] [--engine ts|rust|compare] [--fix] [--json]",
|
|
441
580
|
" klint install-skill [--agents <list>] [--symlink | --copy]",
|
|
442
581
|
"",
|
|
443
582
|
" --config <dir> directory containing klint.yaml or klint.config.json (default: cwd)",
|
|
444
583
|
" --rules <file> custom rules file (default: <configDir>/klint.rules.ts if present)",
|
|
584
|
+
" --engine <name> engine to use: ts (default), rust, or compare (experimental, requires --json)",
|
|
445
585
|
" --fix apply auto-fixes for fixable violations in-place",
|
|
446
586
|
" --json emit structured JSON to stdout (for agent/CI consumption)",
|
|
447
587
|
" --debug print file resolution and rule progress to stderr",
|
package/package.json
CHANGED
|
@@ -26,7 +26,7 @@ export const noConsecutiveArrayPush: KlintRule = {
|
|
|
26
26
|
statements[runStart].getStart()
|
|
27
27
|
);
|
|
28
28
|
violations.push({
|
|
29
|
-
file: relative(root, file),
|
|
29
|
+
file: relative(root, file).replaceAll("\\", "/"),
|
|
30
30
|
line: line + 1,
|
|
31
31
|
message: `${upTo - runStart} consecutive .push() calls on \`${runReceiver}\` — combine into a single .push(a, b, …) call.`,
|
|
32
32
|
});
|
|
@@ -32,7 +32,7 @@ function findNestedTemplate(
|
|
|
32
32
|
if (ts.isTemplateExpression(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
|
|
33
33
|
const { line } = src.getLineAndCharacterOfPosition(node.getStart());
|
|
34
34
|
violations.push({
|
|
35
|
-
file: relative(root, file),
|
|
35
|
+
file: relative(root, file).replaceAll("\\", "/"),
|
|
36
36
|
line: line + 1,
|
|
37
37
|
message:
|
|
38
38
|
"Nested template literal — extract the inner template to a variable to improve readability.",
|
package/rules/no-string-match.ts
CHANGED
|
@@ -45,7 +45,7 @@ export const noStringMatch: KlintRule = {
|
|
|
45
45
|
})()
|
|
46
46
|
: undefined;
|
|
47
47
|
violations.push({
|
|
48
|
-
file: relative(root, file),
|
|
48
|
+
file: relative(root, file).replaceAll("\\", "/"),
|
|
49
49
|
line: s + 1,
|
|
50
50
|
message: `Use RegExp.exec() instead of String.match() for non-global regexes — use new RegExp(${regexText}).exec(${strText}) instead.`,
|
|
51
51
|
fix,
|
|
@@ -23,7 +23,7 @@ export const noUnguardedJsonParse: KlintRule = {
|
|
|
23
23
|
) {
|
|
24
24
|
const { line } = src.getLineAndCharacterOfPosition(node.getStart());
|
|
25
25
|
violations.push({
|
|
26
|
-
file: relative(root, file),
|
|
26
|
+
file: relative(root, file).replaceAll("\\", "/"),
|
|
27
27
|
line: line + 1,
|
|
28
28
|
message:
|
|
29
29
|
"JSON.parse() called without a surrounding try/catch — a malformed payload will throw an unhandled exception.",
|