@konvert7/klint 0.24.1 → 0.25.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 +33 -1
- package/cli/help.ts +41 -0
- package/cli/install-skill.ts +102 -0
- package/cli/output.ts +159 -0
- package/cli/paths.ts +3 -0
- package/cli/rust-config.ts +144 -0
- package/cli/rust-engine.ts +301 -0
- package/cli.ts +15 -733
- package/core/arch.ts +44 -1
- package/core/config.schema.ts +39 -2
- package/core/native-binary.ts +5 -1
- package/core/types.ts +8 -0
- package/package.json +6 -5
- package/skill/klint-rules/SKILL.md +29 -3
package/README.md
CHANGED
|
@@ -98,6 +98,7 @@ klint is for rules that are too project-specific or context-heavy for a formatte
|
|
|
98
98
|
| Singleton ownership | `process.env.API_KEY` may only appear in `src/lib/auth.ts` |
|
|
99
99
|
| Forbidden patterns | `console.log(` is blocked inside hook libraries |
|
|
100
100
|
| Raw JSX elements | `<button>` is blocked outside `src/components/ui/**` |
|
|
101
|
+
| File length | No file in `src/**` may exceed 300 lines |
|
|
101
102
|
| Type-aware mistakes | Promise-returning calls cannot be silently discarded |
|
|
102
103
|
| Repository policy | Custom `klint.rules.ts` rules run with the same severity/config system |
|
|
103
104
|
|
|
@@ -147,7 +148,7 @@ klint --engine compare --json # parity check; rejects unsupported rules
|
|
|
147
148
|
|
|
148
149
|
Rust currently supports:
|
|
149
150
|
|
|
150
|
-
- `arch` rules: imports, forbidden patterns, singleton locations
|
|
151
|
+
- `arch` rules: imports, forbidden patterns (literal + `re:` regex), singleton locations, `maxLines` file limits
|
|
151
152
|
- `no-unguarded-json-parse`
|
|
152
153
|
- `no-sync-in-async`
|
|
153
154
|
- `no-nested-template-literals`
|
|
@@ -274,6 +275,18 @@ arch:
|
|
|
274
275
|
message: "Libraries should return or throw, not terminate the process"
|
|
275
276
|
```
|
|
276
277
|
|
|
278
|
+
Match a **regular expression** instead of a literal by prefixing the pattern with `re:` — useful for value-shaped policies such as off-system utility classes. Regexes use the common JS/RE2 subset (no lookaround or backreferences) so the TS and Rust engines agree:
|
|
279
|
+
|
|
280
|
+
```yaml
|
|
281
|
+
arch:
|
|
282
|
+
forbidden:
|
|
283
|
+
- pattern: 're:\b(?:p|gap)-\[' # reject Tailwind arbitrary values like p-[18px]
|
|
284
|
+
in: ["src/**/*.tsx"]
|
|
285
|
+
message: "Use the spacing scale, not arbitrary bracket values"
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
A literal pattern that itself begins with `re:` cannot be expressed — such a value is always read as a regex.
|
|
289
|
+
|
|
277
290
|
Block raw JSX/HTML elements with `jsx-element` to push consumers onto design-system primitives. It is AST-matched on intrinsic (lowercase) tag names, so it is robust to whitespace, attributes, and lookalikes like `<buttonGroup>`:
|
|
278
291
|
|
|
279
292
|
```yaml
|
|
@@ -299,6 +312,8 @@ arch:
|
|
|
299
312
|
message: "Use the auth module"
|
|
300
313
|
```
|
|
301
314
|
|
|
315
|
+
Like `forbidden`, a singleton `pattern` may use the `re:` regex prefix for value-shaped matches.
|
|
316
|
+
|
|
302
317
|
`singleton` accepts `jsx-element` too — pin a raw element to the one primitive file that is allowed to render it:
|
|
303
318
|
|
|
304
319
|
```yaml
|
|
@@ -310,6 +325,23 @@ arch:
|
|
|
310
325
|
message: "Raw <button> belongs only to the Button primitive"
|
|
311
326
|
```
|
|
312
327
|
|
|
328
|
+
### Max File Length
|
|
329
|
+
|
|
330
|
+
Cap how long any file in a layer may grow. The limit counts every physical line — blanks and comments included — and a file over it is flagged at the first line past the limit:
|
|
331
|
+
|
|
332
|
+
```yaml
|
|
333
|
+
arch:
|
|
334
|
+
maxLines:
|
|
335
|
+
- limit: 300
|
|
336
|
+
in: ["src/**"]
|
|
337
|
+
message: "Split this module" # optional — defaults to a message naming the limit
|
|
338
|
+
|
|
339
|
+
- limit: 600 # different ceilings per scope
|
|
340
|
+
in: ["tests/**"]
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
`maxLines` is enforced in both the TS and Rust engines and applies to every language klint scans, with no formatter dependency.
|
|
344
|
+
|
|
313
345
|
### Python Architecture Checks
|
|
314
346
|
|
|
315
347
|
The Rust engine can apply architecture rules to Python projects:
|
package/cli/help.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { BUILT_IN_PLUGINS } from "../plugins/index";
|
|
2
|
+
import { BUILT_IN_RULES } from "../rules/index";
|
|
3
|
+
|
|
4
|
+
export function printHelp(): void {
|
|
5
|
+
const pluginRules = new Set(
|
|
6
|
+
Object.values(BUILT_IN_PLUGINS).flatMap((p) => Object.keys(p.rules))
|
|
7
|
+
);
|
|
8
|
+
const standaloneRules = Object.keys(BUILT_IN_RULES).filter((r) => !pluginRules.has(r));
|
|
9
|
+
const pluginEntries = Object.entries(BUILT_IN_PLUGINS);
|
|
10
|
+
|
|
11
|
+
process.stdout.write(
|
|
12
|
+
[
|
|
13
|
+
"klint — agent harness for TypeScript architecture rules",
|
|
14
|
+
"",
|
|
15
|
+
"Usage: klint [--config <dir>] [--rules <file>] [--engine ts|rust|compare|auto] [--fix] [--json]",
|
|
16
|
+
" klint install-skill [--agents <list>] [--symlink | --copy]",
|
|
17
|
+
"",
|
|
18
|
+
" --config <dir> directory containing klint.yaml or klint.config.json (default: cwd)",
|
|
19
|
+
" --rules <file> custom rules file (default: <configDir>/klint.rules.ts if present)",
|
|
20
|
+
" --engine <name> engine to use: ts (default), rust, compare, or auto (experimental)",
|
|
21
|
+
" --fix apply auto-fixes for fixable violations in-place",
|
|
22
|
+
" --json emit structured JSON to stdout (for agent/CI consumption)",
|
|
23
|
+
" --debug print file resolution and rule progress to stderr",
|
|
24
|
+
"",
|
|
25
|
+
" install-skill install the rule-authoring skill into agent config directories",
|
|
26
|
+
" --agents <list> comma-separated: claude,opencode,cursor,codex (default: all)",
|
|
27
|
+
" --symlink install as symlink (stays in sync with updates)",
|
|
28
|
+
" --copy install as copy (default in non-TTY)",
|
|
29
|
+
"",
|
|
30
|
+
`Built-in rules (${standaloneRules.length}):`,
|
|
31
|
+
...standaloneRules.map((r) => ` ${r}`),
|
|
32
|
+
"",
|
|
33
|
+
`Plugins (${pluginEntries.length}):`,
|
|
34
|
+
...pluginEntries.flatMap(([name, plugin]) => [
|
|
35
|
+
` ${name}`,
|
|
36
|
+
...Object.keys(plugin.rules).map((r) => ` ${r}`),
|
|
37
|
+
]),
|
|
38
|
+
"",
|
|
39
|
+
].join("\n")
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { cp, mkdir, rm, symlink } from "node:fs/promises";
|
|
3
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
4
|
+
import * as clack from "@clack/prompts";
|
|
5
|
+
import { PACKAGE_ROOT } from "./paths";
|
|
6
|
+
|
|
7
|
+
const AGENT_TARGETS = [
|
|
8
|
+
{ value: "claude", label: "Claude Code" },
|
|
9
|
+
{ value: "opencode", label: "opencode" },
|
|
10
|
+
{ value: "cursor", label: "Cursor" },
|
|
11
|
+
{ value: "codex", label: "Codex" },
|
|
12
|
+
] as const;
|
|
13
|
+
|
|
14
|
+
type AgentKey = (typeof AGENT_TARGETS)[number]["value"];
|
|
15
|
+
|
|
16
|
+
const AGENT_DIRS: Record<AgentKey, string> = {
|
|
17
|
+
claude: ".claude/skills",
|
|
18
|
+
opencode: ".agents/skills",
|
|
19
|
+
cursor: ".cursor/skills",
|
|
20
|
+
codex: ".agents/skills",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export async function installSkill(args: string[]): Promise<void> {
|
|
24
|
+
const skillSrc = join(PACKAGE_ROOT, "skill", "klint-rules");
|
|
25
|
+
if (!existsSync(skillSrc)) {
|
|
26
|
+
process.stderr.write(`klint: skill source not found at ${skillSrc}\n`);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Parse non-interactive flags
|
|
31
|
+
let flagAgents: AgentKey[] | undefined;
|
|
32
|
+
let flagSymlink: boolean | undefined;
|
|
33
|
+
for (let i = 0; i < args.length; i++) {
|
|
34
|
+
if (args[i] === "--agents" && args[i + 1])
|
|
35
|
+
flagAgents = args[++i].split(",") as AgentKey[];
|
|
36
|
+
else if (args[i] === "--symlink") flagSymlink = true;
|
|
37
|
+
else if (args[i] === "--copy") flagSymlink = false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let selectedAgents: AgentKey[];
|
|
41
|
+
let useSymlink: boolean;
|
|
42
|
+
|
|
43
|
+
if (!process.stdin.isTTY || flagAgents !== undefined || flagSymlink !== undefined) {
|
|
44
|
+
selectedAgents = flagAgents ?? (AGENT_TARGETS.map((a) => a.value) as AgentKey[]);
|
|
45
|
+
useSymlink = flagSymlink ?? false;
|
|
46
|
+
} else {
|
|
47
|
+
clack.intro("klint install-skill");
|
|
48
|
+
|
|
49
|
+
const agents = await clack.multiselect<AgentKey>({
|
|
50
|
+
message: "Which agents should the skill be installed for?",
|
|
51
|
+
options: AGENT_TARGETS.map((a) => ({ value: a.value, label: a.label })),
|
|
52
|
+
initialValues: AGENT_TARGETS.map((a) => a.value) as AgentKey[],
|
|
53
|
+
});
|
|
54
|
+
if (clack.isCancel(agents)) {
|
|
55
|
+
clack.cancel("Cancelled.");
|
|
56
|
+
process.exit(0);
|
|
57
|
+
}
|
|
58
|
+
selectedAgents = agents as AgentKey[];
|
|
59
|
+
|
|
60
|
+
const mode = await clack.select<"symlink" | "copy">({
|
|
61
|
+
message: "Install as symlink or copy?",
|
|
62
|
+
options: [
|
|
63
|
+
{
|
|
64
|
+
value: "symlink",
|
|
65
|
+
label: "Symlink",
|
|
66
|
+
hint: "stays in sync when klint updates",
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
value: "copy",
|
|
70
|
+
label: "Copy",
|
|
71
|
+
hint: "one-time snapshot, no ongoing dependency",
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
});
|
|
75
|
+
if (clack.isCancel(mode)) {
|
|
76
|
+
clack.cancel("Cancelled.");
|
|
77
|
+
process.exit(0);
|
|
78
|
+
}
|
|
79
|
+
useSymlink = mode === "symlink";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const cwd = process.cwd();
|
|
83
|
+
const linkType = process.platform === "win32" ? "junction" : "dir";
|
|
84
|
+
for (const key of selectedAgents) {
|
|
85
|
+
const dest = resolve(cwd, AGENT_DIRS[key], "klint-rules");
|
|
86
|
+
await mkdir(dirname(dest), { recursive: true });
|
|
87
|
+
try {
|
|
88
|
+
await rm(dest, { recursive: true, force: true });
|
|
89
|
+
} catch {
|
|
90
|
+
/* already gone */
|
|
91
|
+
}
|
|
92
|
+
if (useSymlink) {
|
|
93
|
+
await symlink(relative(dirname(dest), skillSrc), dest, linkType);
|
|
94
|
+
} else {
|
|
95
|
+
await cp(skillSrc, dest, { recursive: true });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (process.stdin.isTTY) {
|
|
100
|
+
clack.outro("Done.");
|
|
101
|
+
}
|
|
102
|
+
}
|
package/cli/output.ts
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import type { KlintDebugEvent, runKlint } from "../core/runner";
|
|
2
|
+
import type { Violation } from "../core/types";
|
|
3
|
+
|
|
4
|
+
export function formatDuration(startedAt: number): string {
|
|
5
|
+
const elapsedMs = performance.now() - startedAt;
|
|
6
|
+
if (elapsedMs < 1000) return `${Math.round(elapsedMs)}ms`;
|
|
7
|
+
return `${(elapsedMs / 1000).toFixed(elapsedMs < 10_000 ? 2 : 1)}s`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function writeTextOutput(
|
|
11
|
+
violations: Array<Omit<Violation, "fix">>,
|
|
12
|
+
startedAt: number
|
|
13
|
+
): never {
|
|
14
|
+
const errors = violations.filter((v) => v.severity === "error");
|
|
15
|
+
const warns = violations.filter((v) => v.severity === "warn");
|
|
16
|
+
|
|
17
|
+
if (errors.length === 0 && warns.length === 0) {
|
|
18
|
+
process.stdout.write(`klint: 0 violations in ${formatDuration(startedAt)}\n`);
|
|
19
|
+
process.exit(0);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const formatBlock = (v: (typeof violations)[number]) => {
|
|
23
|
+
const prefix = v.severity === "warn" ? "⚠" : "×";
|
|
24
|
+
const header = `${v.file}:${v.line} [${v.rule}]`;
|
|
25
|
+
const sep = "━".repeat(Math.max(0, 80 - header.length));
|
|
26
|
+
return `${header} ${sep}\n\n ${prefix} ${v.message}\n`;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
if (warns.length > 0) {
|
|
30
|
+
process.stderr.write(
|
|
31
|
+
`klint: ${warns.length} warning(s)\n\n${warns.map(formatBlock).join("\n")}`
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
if (errors.length > 0) {
|
|
35
|
+
process.stderr.write(
|
|
36
|
+
`klint: ${errors.length} error(s)\n\n${errors.map(formatBlock).join("\n")}`
|
|
37
|
+
);
|
|
38
|
+
process.stderr.write(`\nklint: finished in ${formatDuration(startedAt)}\n`);
|
|
39
|
+
process.exit(2);
|
|
40
|
+
}
|
|
41
|
+
process.stderr.write(`\nklint: finished in ${formatDuration(startedAt)}\n`);
|
|
42
|
+
process.exit(0);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface JsonPayload {
|
|
46
|
+
violations: Array<Omit<Violation, "fix"> & { fix: unknown }>;
|
|
47
|
+
summary: {
|
|
48
|
+
errors: number;
|
|
49
|
+
warnings: number;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function emptyJsonPayload(): JsonPayload {
|
|
54
|
+
return {
|
|
55
|
+
violations: [],
|
|
56
|
+
summary: { errors: 0, warnings: 0 },
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function toJsonPayload(violations: ReturnType<typeof runKlint>): JsonPayload {
|
|
61
|
+
const errors = violations.filter((v) => v.severity === "error");
|
|
62
|
+
return {
|
|
63
|
+
violations: violations.map((v) => ({ ...v, fix: v.fix ?? null })),
|
|
64
|
+
summary: { errors: errors.length, warnings: violations.length - errors.length },
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function mergeJsonOutputs(...outputs: JsonPayload[]): JsonPayload {
|
|
69
|
+
const violations = outputs
|
|
70
|
+
.flatMap((output) => output.violations)
|
|
71
|
+
.sort(
|
|
72
|
+
(a, b) =>
|
|
73
|
+
a.file.localeCompare(b.file) ||
|
|
74
|
+
a.line - b.line ||
|
|
75
|
+
a.rule.localeCompare(b.rule) ||
|
|
76
|
+
a.message.localeCompare(b.message)
|
|
77
|
+
);
|
|
78
|
+
const errors = violations.filter((violation) => violation.severity === "error").length;
|
|
79
|
+
return {
|
|
80
|
+
violations,
|
|
81
|
+
summary: {
|
|
82
|
+
errors,
|
|
83
|
+
warnings: violations.length - errors,
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function isJsonPayload(value: unknown): value is JsonPayload {
|
|
89
|
+
if (!value || typeof value !== "object") return false;
|
|
90
|
+
const candidate = value as Partial<JsonPayload>;
|
|
91
|
+
return Array.isArray(candidate.violations) && typeof candidate.summary === "object";
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function toJsonCliResult(violations: ReturnType<typeof runKlint>): {
|
|
95
|
+
stdout: string;
|
|
96
|
+
status: number;
|
|
97
|
+
} {
|
|
98
|
+
const payload = toJsonPayload(violations);
|
|
99
|
+
return {
|
|
100
|
+
stdout: JSON.stringify(payload),
|
|
101
|
+
status: payload.summary.errors > 0 ? 2 : 0,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function writeDebugEvent(event: KlintDebugEvent): void {
|
|
106
|
+
switch (event.type) {
|
|
107
|
+
case "walk:start":
|
|
108
|
+
process.stderr.write(`[klint:debug] walk ${event.dir}\n`);
|
|
109
|
+
break;
|
|
110
|
+
case "walk:done":
|
|
111
|
+
process.stderr.write(`[klint:debug] walked ${event.dir} (${event.files} files)\n`);
|
|
112
|
+
break;
|
|
113
|
+
case "files:resolved":
|
|
114
|
+
process.stderr.write(`[klint:debug] resolved ${event.files} files\n`);
|
|
115
|
+
break;
|
|
116
|
+
case "rule:start":
|
|
117
|
+
process.stderr.write(
|
|
118
|
+
`[klint:debug] rule ${event.rule} start (${event.files} files)\n`
|
|
119
|
+
);
|
|
120
|
+
break;
|
|
121
|
+
case "rule:done":
|
|
122
|
+
process.stderr.write(
|
|
123
|
+
`[klint:debug] rule ${event.rule} done (${event.violations} violations)\n`
|
|
124
|
+
);
|
|
125
|
+
break;
|
|
126
|
+
case "arch:start":
|
|
127
|
+
process.stderr.write(`[klint:debug] arch start (${event.files} files)\n`);
|
|
128
|
+
break;
|
|
129
|
+
case "arch:done":
|
|
130
|
+
process.stderr.write(`[klint:debug] arch done (${event.violations} violations)\n`);
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function parseJsonPayload(value: string): unknown {
|
|
136
|
+
try {
|
|
137
|
+
return JSON.parse(value);
|
|
138
|
+
} catch {
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function jsonPayloadEquals(a: unknown, b: unknown): boolean {
|
|
144
|
+
return JSON.stringify(canonicalJson(a)) === JSON.stringify(canonicalJson(b));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function canonicalJson(value: unknown): unknown {
|
|
148
|
+
if (Array.isArray(value)) {
|
|
149
|
+
return value
|
|
150
|
+
.map(canonicalJson)
|
|
151
|
+
.sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right)));
|
|
152
|
+
}
|
|
153
|
+
if (!value || typeof value !== "object") return value;
|
|
154
|
+
return Object.fromEntries(
|
|
155
|
+
Object.entries(value)
|
|
156
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
157
|
+
.map(([key, entry]) => [key, canonicalJson(entry)])
|
|
158
|
+
);
|
|
159
|
+
}
|
package/cli/paths.ts
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { resolveNativePackageBinary } from "../core/native-binary";
|
|
4
|
+
import type { RuleConfigValue } from "../core/types";
|
|
5
|
+
import { BUILT_IN_PLUGINS } from "../plugins/index";
|
|
6
|
+
import { PACKAGE_ROOT } from "./paths";
|
|
7
|
+
|
|
8
|
+
const RUST_SUPPORTED_RULES = new Set([
|
|
9
|
+
"no-consecutive-array-push",
|
|
10
|
+
"no-nested-template-literals",
|
|
11
|
+
"no-string-match",
|
|
12
|
+
"no-sync-in-async",
|
|
13
|
+
"no-unguarded-json-parse",
|
|
14
|
+
"sonar/no-single-char-class",
|
|
15
|
+
"sonar/prefer-at",
|
|
16
|
+
"sonar/prefer-nullish-coalescing-assign",
|
|
17
|
+
"sonar/prefer-string-replaceall",
|
|
18
|
+
"sonar/prefer-string-raw",
|
|
19
|
+
"sonar/prefer-string-raw-regexp",
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
export function splitRulesForAuto(rules: Record<string, RuleConfigValue>): {
|
|
23
|
+
rustRules: Record<string, RuleConfigValue>;
|
|
24
|
+
tsRules: Record<string, RuleConfigValue>;
|
|
25
|
+
} {
|
|
26
|
+
const rustRules: Record<string, RuleConfigValue> = {};
|
|
27
|
+
const tsRules: Record<string, RuleConfigValue> = {};
|
|
28
|
+
|
|
29
|
+
for (const [name, value] of Object.entries(rules)) {
|
|
30
|
+
if (RUST_SUPPORTED_RULES.has(name)) rustRules[name] = value;
|
|
31
|
+
else tsRules[name] = value;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return { rustRules, tsRules };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface RustEngineCommand {
|
|
38
|
+
bin: string;
|
|
39
|
+
args: string[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function resolveRustEngineCommand(configDir: string): RustEngineCommand {
|
|
43
|
+
const args = ["--config", configDir, "--json"];
|
|
44
|
+
const explicitBin = process.env.KLINT_RUST_BIN;
|
|
45
|
+
if (explicitBin) {
|
|
46
|
+
return { bin: explicitBin, args };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const localBin = join(
|
|
50
|
+
PACKAGE_ROOT,
|
|
51
|
+
"target",
|
|
52
|
+
"debug",
|
|
53
|
+
process.platform === "win32" ? "klint-rs.exe" : "klint-rs"
|
|
54
|
+
);
|
|
55
|
+
if (isSourceCheckout() && existsSync(localBin)) {
|
|
56
|
+
return { bin: localBin, args };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const nativePackageBin = resolveNativePackageBinary({ packageRoot: PACKAGE_ROOT });
|
|
60
|
+
if (nativePackageBin) {
|
|
61
|
+
return { bin: nativePackageBin, args };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!isSourceCheckout()) {
|
|
65
|
+
process.stderr.write(
|
|
66
|
+
"klint: native Rust engine binary is unavailable for this package version\n"
|
|
67
|
+
);
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
bin: "cargo",
|
|
73
|
+
args: ["run", "--quiet", "-p", "klint-rs", "--", ...args],
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function isSourceCheckout(): boolean {
|
|
78
|
+
return existsSync(join(PACKAGE_ROOT, "crates", "klint-rs", "Cargo.toml"));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function rustEngineUnsupportedReason({
|
|
82
|
+
fix,
|
|
83
|
+
raw,
|
|
84
|
+
rulesFile,
|
|
85
|
+
}: {
|
|
86
|
+
fix: boolean;
|
|
87
|
+
raw: {
|
|
88
|
+
plugins?: string[];
|
|
89
|
+
rules?: Record<string, RuleConfigValue>;
|
|
90
|
+
arch?: unknown;
|
|
91
|
+
};
|
|
92
|
+
rulesFile?: string;
|
|
93
|
+
}): string | undefined {
|
|
94
|
+
if (fix) return "KLINT_ENGINE=rust does not support --fix";
|
|
95
|
+
if (rulesFile) return "KLINT_ENGINE=rust does not support --rules";
|
|
96
|
+
let effectiveRules: Record<string, RuleConfigValue>;
|
|
97
|
+
try {
|
|
98
|
+
effectiveRules = resolveEffectiveRules(raw.plugins, raw.rules ?? {});
|
|
99
|
+
} catch (error) {
|
|
100
|
+
return error instanceof Error
|
|
101
|
+
? error.message
|
|
102
|
+
: "KLINT_ENGINE=rust failed to load plugins";
|
|
103
|
+
}
|
|
104
|
+
const activeSupportedRules = Object.entries(effectiveRules).filter(
|
|
105
|
+
([name, value]) =>
|
|
106
|
+
ruleConfigSeverity(value) !== "off" && RUST_SUPPORTED_RULES.has(name)
|
|
107
|
+
);
|
|
108
|
+
const unsupportedRules = Object.entries(effectiveRules)
|
|
109
|
+
.filter(
|
|
110
|
+
([name, value]) =>
|
|
111
|
+
ruleConfigSeverity(value) !== "off" && !RUST_SUPPORTED_RULES.has(name)
|
|
112
|
+
)
|
|
113
|
+
.map(([name]) => name);
|
|
114
|
+
if (!raw.arch && activeSupportedRules.length === 0 && unsupportedRules.length === 0) {
|
|
115
|
+
return "KLINT_ENGINE=rust requires an arch config or supported rule";
|
|
116
|
+
}
|
|
117
|
+
if (unsupportedRules.length > 0) {
|
|
118
|
+
return [
|
|
119
|
+
"Rust engine currently supports arch rules and selected rules only",
|
|
120
|
+
"",
|
|
121
|
+
"Unsupported rules:",
|
|
122
|
+
...unsupportedRules.map((rule) => `- ${rule}`),
|
|
123
|
+
].join("\n");
|
|
124
|
+
}
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function resolveEffectiveRules(
|
|
129
|
+
plugins: string[] | undefined,
|
|
130
|
+
rules: Record<string, RuleConfigValue>
|
|
131
|
+
): Record<string, RuleConfigValue> {
|
|
132
|
+
const pluginDefaults: Record<string, RuleConfigValue> = {};
|
|
133
|
+
for (const pluginName of plugins ?? []) {
|
|
134
|
+
const plugin = BUILT_IN_PLUGINS[pluginName];
|
|
135
|
+
if (!plugin) throw new Error(`Unknown klint plugin: "${pluginName}"`);
|
|
136
|
+
Object.assign(pluginDefaults, plugin.rules);
|
|
137
|
+
}
|
|
138
|
+
return { ...pluginDefaults, ...rules };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function ruleConfigSeverity(value: RuleConfigValue): string | undefined {
|
|
142
|
+
if (typeof value === "string") return value;
|
|
143
|
+
return value.severity;
|
|
144
|
+
}
|