@konvert7/klint 0.29.0 → 0.31.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 -0
- package/cli/output.ts +19 -0
- package/cli/rust-engine.ts +13 -6
- package/cli.ts +31 -5
- package/core/arch-comments.ts +14 -0
- package/core/config.schema.ts +2 -2
- package/core/schema-version.ts +46 -0
- package/core/types.ts +1 -1
- package/klint.schema.json +1275 -0
- package/klint.schema.yaml +999 -0
- package/package.json +8 -6
- package/tools/generate-schema.ts +12 -2
- package/tools/semantic-release-native-npm.js +3 -1
- package/tools/semantic-release-root-npm.js +15 -1
package/README.md
CHANGED
|
@@ -209,6 +209,39 @@ arch:
|
|
|
209
209
|
|
|
210
210
|
`klint.config.json` is still supported for backwards compatibility.
|
|
211
211
|
|
|
212
|
+
### Editor support
|
|
213
|
+
|
|
214
|
+
The package ships `klint.schema.json` and `klint.schema.yaml`, so the recommended
|
|
215
|
+
setup points at the installed copy:
|
|
216
|
+
|
|
217
|
+
```yaml
|
|
218
|
+
$schema: ./node_modules/@konvert7/klint/klint.schema.json
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
That file is installed alongside klint itself, so it cannot drift — upgrading klint
|
|
222
|
+
upgrades the schema with it. Both the inline `$schema` key above and the
|
|
223
|
+
`# yaml-language-server: $schema=…` modeline work; when both are present the modeline
|
|
224
|
+
wins. `klint.config.json` files are handled by the editor's built-in JSON support.
|
|
225
|
+
|
|
226
|
+
### Pinning the schema version
|
|
227
|
+
|
|
228
|
+
To pin a specific published schema instead, set `$schema` to a version-tagged URL:
|
|
229
|
+
|
|
230
|
+
```yaml
|
|
231
|
+
$schema: https://raw.githubusercontent.com/konvert7/klint/refs/tags/v0.29.0/klint.schema.json
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
When the tagged version differs from the installed klint, the run reports a
|
|
235
|
+
`klint/schema-version` warning naming both versions and the URL to move to. It is a
|
|
236
|
+
warning, not an error — the exit code is unaffected. Local paths carry no version and
|
|
237
|
+
are never checked, which is why the installed copy above never warns.
|
|
238
|
+
|
|
239
|
+
This check runs when klint runs. Editors validate the config's *shape* against the
|
|
240
|
+
schema, but have no way to compare it against the installed klint, so the warning
|
|
241
|
+
appears in klint's output rather than as an editor diagnostic.
|
|
242
|
+
|
|
243
|
+
`klint --version` prints the installed version.
|
|
244
|
+
|
|
212
245
|
## Architecture as Code
|
|
213
246
|
|
|
214
247
|
The `arch:` section turns project boundaries into executable policy.
|
package/cli/output.ts
CHANGED
|
@@ -85,6 +85,25 @@ export function mergeJsonOutputs(...outputs: JsonPayload[]): JsonPayload {
|
|
|
85
85
|
};
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
export function withAdvisories(
|
|
89
|
+
stdout: string,
|
|
90
|
+
advisories: Array<Omit<Violation, "fix">>
|
|
91
|
+
): string {
|
|
92
|
+
if (advisories.length === 0) return stdout;
|
|
93
|
+
const payload = parseJsonPayload(stdout);
|
|
94
|
+
if (!isJsonPayload(payload)) return stdout;
|
|
95
|
+
|
|
96
|
+
const violations = [
|
|
97
|
+
...payload.violations,
|
|
98
|
+
...advisories.map((advisory) => ({ ...advisory, fix: null })),
|
|
99
|
+
];
|
|
100
|
+
const errors = violations.filter((v) => v.severity === "error").length;
|
|
101
|
+
return JSON.stringify({
|
|
102
|
+
violations,
|
|
103
|
+
summary: { errors, warnings: violations.length - errors },
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
88
107
|
export function isJsonPayload(value: unknown): value is JsonPayload {
|
|
89
108
|
if (!value || typeof value !== "object") return false;
|
|
90
109
|
const candidate = value as Partial<JsonPayload>;
|
package/cli/rust-engine.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { runKlint } from "../core/runner";
|
|
6
|
-
import type { KlintRule, RuleConfigValue } from "../core/types";
|
|
6
|
+
import type { KlintRule, RuleConfigValue, Violation } from "../core/types";
|
|
7
7
|
import {
|
|
8
8
|
emptyJsonPayload,
|
|
9
9
|
isJsonPayload,
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
parseJsonPayload,
|
|
14
14
|
toJsonCliResult,
|
|
15
15
|
toJsonPayload,
|
|
16
|
+
withAdvisories,
|
|
16
17
|
writeTextOutput,
|
|
17
18
|
} from "./output";
|
|
18
19
|
import { PACKAGE_ROOT } from "./paths";
|
|
@@ -25,6 +26,7 @@ import {
|
|
|
25
26
|
} from "./rust-config";
|
|
26
27
|
|
|
27
28
|
export function runRustEngine({
|
|
29
|
+
advisories,
|
|
28
30
|
fix,
|
|
29
31
|
json,
|
|
30
32
|
raw,
|
|
@@ -32,6 +34,7 @@ export function runRustEngine({
|
|
|
32
34
|
rulesFile,
|
|
33
35
|
startedAt,
|
|
34
36
|
}: {
|
|
37
|
+
advisories: Array<Omit<Violation, "fix">>;
|
|
35
38
|
fix: boolean;
|
|
36
39
|
json: boolean;
|
|
37
40
|
raw: {
|
|
@@ -73,7 +76,7 @@ export function runRustEngine({
|
|
|
73
76
|
|
|
74
77
|
if (result.stderr) process.stderr.write(result.stderr);
|
|
75
78
|
if (json) {
|
|
76
|
-
if (result.stdout) process.stdout.write(result.stdout);
|
|
79
|
+
if (result.stdout) process.stdout.write(withAdvisories(result.stdout, advisories));
|
|
77
80
|
process.exit(result.status ?? 1);
|
|
78
81
|
}
|
|
79
82
|
|
|
@@ -82,10 +85,11 @@ export function runRustEngine({
|
|
|
82
85
|
process.stderr.write("klint: --engine rust failed to parse Rust JSON output\n");
|
|
83
86
|
process.exit(1);
|
|
84
87
|
}
|
|
85
|
-
writeTextOutput(payload.violations, startedAt);
|
|
88
|
+
writeTextOutput([...payload.violations, ...advisories], startedAt);
|
|
86
89
|
}
|
|
87
90
|
|
|
88
91
|
export function runCompareEngine({
|
|
92
|
+
advisories,
|
|
89
93
|
fix,
|
|
90
94
|
json,
|
|
91
95
|
raw,
|
|
@@ -93,6 +97,7 @@ export function runCompareEngine({
|
|
|
93
97
|
rulesFile,
|
|
94
98
|
tsViolations,
|
|
95
99
|
}: {
|
|
100
|
+
advisories: Array<Omit<Violation, "fix">>;
|
|
96
101
|
fix: boolean;
|
|
97
102
|
json: boolean;
|
|
98
103
|
raw: {
|
|
@@ -154,7 +159,7 @@ export function runCompareEngine({
|
|
|
154
159
|
process.exit(1);
|
|
155
160
|
}
|
|
156
161
|
|
|
157
|
-
process.stdout.write(tsResult.stdout);
|
|
162
|
+
process.stdout.write(withAdvisories(tsResult.stdout, advisories));
|
|
158
163
|
process.exit(tsResult.status);
|
|
159
164
|
}
|
|
160
165
|
|
|
@@ -188,6 +193,7 @@ function writeRustEngineConfig({
|
|
|
188
193
|
}
|
|
189
194
|
|
|
190
195
|
export function runAutoEngine({
|
|
196
|
+
advisories,
|
|
191
197
|
fix,
|
|
192
198
|
json,
|
|
193
199
|
raw,
|
|
@@ -197,6 +203,7 @@ export function runAutoEngine({
|
|
|
197
203
|
customRules,
|
|
198
204
|
customRulesMap,
|
|
199
205
|
}: {
|
|
206
|
+
advisories: Array<Omit<Violation, "fix">>;
|
|
200
207
|
fix: boolean;
|
|
201
208
|
json: boolean;
|
|
202
209
|
raw: {
|
|
@@ -241,10 +248,10 @@ export function runAutoEngine({
|
|
|
241
248
|
const tsOutput = toJsonPayload(tsViolations);
|
|
242
249
|
const merged = mergeJsonOutputs(rustOutput, tsOutput);
|
|
243
250
|
if (json) {
|
|
244
|
-
process.stdout.write(JSON.stringify(merged));
|
|
251
|
+
process.stdout.write(withAdvisories(JSON.stringify(merged), advisories));
|
|
245
252
|
process.exit(merged.summary.errors > 0 ? 2 : 0);
|
|
246
253
|
}
|
|
247
|
-
writeTextOutput(merged.violations, startedAt);
|
|
254
|
+
writeTextOutput([...merged.violations, ...advisories], startedAt);
|
|
248
255
|
}
|
|
249
256
|
|
|
250
257
|
function runAutoRustSubset({
|
package/cli.ts
CHANGED
|
@@ -8,14 +8,26 @@ import { installSkill } from "./cli/install-skill";
|
|
|
8
8
|
import {
|
|
9
9
|
formatDuration,
|
|
10
10
|
toJsonCliResult,
|
|
11
|
+
withAdvisories,
|
|
11
12
|
writeDebugEvent,
|
|
12
13
|
writeTextOutput,
|
|
13
14
|
} from "./cli/output";
|
|
15
|
+
import { PACKAGE_ROOT } from "./cli/paths";
|
|
14
16
|
import { runAutoEngine, runCompareEngine, runRustEngine } from "./cli/rust-engine";
|
|
15
17
|
import { applyFixes } from "./core/fixer";
|
|
16
18
|
import { runKlint } from "./core/runner";
|
|
19
|
+
import { schemaVersionAdvisory } from "./core/schema-version";
|
|
17
20
|
import type { ArchConfig, KlintConfig, KlintRule, RuleConfigValue } from "./core/types";
|
|
18
21
|
|
|
22
|
+
async function installedVersion(): Promise<string> {
|
|
23
|
+
const manifest = await readFile(resolve(PACKAGE_ROOT, "package.json"), "utf-8");
|
|
24
|
+
try {
|
|
25
|
+
return (JSON.parse(manifest) as { version?: string }).version ?? "0.0.0";
|
|
26
|
+
} catch {
|
|
27
|
+
return "0.0.0";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
19
31
|
interface CliOptions {
|
|
20
32
|
configDir?: string;
|
|
21
33
|
rulesFile?: string;
|
|
@@ -52,6 +64,9 @@ export async function main(opts: CliOptions = {}): Promise<void> {
|
|
|
52
64
|
else if (args[i] === "--help" || args[i] === "-h") {
|
|
53
65
|
printHelp();
|
|
54
66
|
process.exit(0);
|
|
67
|
+
} else if (args[i] === "--version" || args[i] === "-V") {
|
|
68
|
+
process.stdout.write(`klint ${await installedVersion()}\n`);
|
|
69
|
+
process.exit(0);
|
|
55
70
|
}
|
|
56
71
|
}
|
|
57
72
|
|
|
@@ -70,6 +85,7 @@ export async function main(opts: CliOptions = {}): Promise<void> {
|
|
|
70
85
|
}
|
|
71
86
|
|
|
72
87
|
interface RawConfig {
|
|
88
|
+
$schema?: string;
|
|
73
89
|
root?: string;
|
|
74
90
|
include?: string[];
|
|
75
91
|
plugins?: string[];
|
|
@@ -77,17 +93,25 @@ export async function main(opts: CliOptions = {}): Promise<void> {
|
|
|
77
93
|
arch?: unknown;
|
|
78
94
|
}
|
|
79
95
|
let raw: RawConfig;
|
|
96
|
+
let configText = "";
|
|
80
97
|
try {
|
|
81
|
-
|
|
82
|
-
raw = (usingYaml ? parseYaml(
|
|
98
|
+
configText = await readFile(configPath, "utf-8");
|
|
99
|
+
raw = (usingYaml ? parseYaml(configText) : JSON.parse(configText)) as RawConfig;
|
|
83
100
|
} catch {
|
|
84
101
|
process.stderr.write(`klint: failed to parse ${configPath}\n`);
|
|
85
102
|
process.exit(1);
|
|
86
103
|
}
|
|
87
104
|
const root = resolve(configDir, raw.root ?? ".");
|
|
88
105
|
|
|
106
|
+
const advisories = schemaVersionAdvisory({
|
|
107
|
+
schema: raw.$schema,
|
|
108
|
+
installed: await installedVersion(),
|
|
109
|
+
configFile: usingYaml ? "klint.yaml" : "klint.config.json",
|
|
110
|
+
configText,
|
|
111
|
+
});
|
|
112
|
+
|
|
89
113
|
if (engine === "rust") {
|
|
90
|
-
runRustEngine({ fix, json, raw, root, rulesFile, startedAt });
|
|
114
|
+
runRustEngine({ advisories, fix, json, raw, root, rulesFile, startedAt });
|
|
91
115
|
return;
|
|
92
116
|
}
|
|
93
117
|
if (
|
|
@@ -119,6 +143,7 @@ export async function main(opts: CliOptions = {}): Promise<void> {
|
|
|
119
143
|
|
|
120
144
|
if (engine === "auto") {
|
|
121
145
|
runAutoEngine({
|
|
146
|
+
advisories,
|
|
122
147
|
fix,
|
|
123
148
|
json,
|
|
124
149
|
raw,
|
|
@@ -145,6 +170,7 @@ export async function main(opts: CliOptions = {}): Promise<void> {
|
|
|
145
170
|
|
|
146
171
|
if (engine === "compare") {
|
|
147
172
|
runCompareEngine({
|
|
173
|
+
advisories,
|
|
148
174
|
fix,
|
|
149
175
|
json,
|
|
150
176
|
raw,
|
|
@@ -157,7 +183,7 @@ export async function main(opts: CliOptions = {}): Promise<void> {
|
|
|
157
183
|
|
|
158
184
|
if (json) {
|
|
159
185
|
const tsResult = toJsonCliResult(violations);
|
|
160
|
-
process.stdout.write(tsResult.stdout);
|
|
186
|
+
process.stdout.write(withAdvisories(tsResult.stdout, advisories));
|
|
161
187
|
process.exit(tsResult.status);
|
|
162
188
|
}
|
|
163
189
|
|
|
@@ -189,7 +215,7 @@ export async function main(opts: CliOptions = {}): Promise<void> {
|
|
|
189
215
|
process.stdout.write(`${msg} Finished in ${formatDuration(startedAt)}.\n`);
|
|
190
216
|
process.exit(0);
|
|
191
217
|
}
|
|
192
|
-
writeTextOutput(violations, startedAt);
|
|
218
|
+
writeTextOutput([...violations, ...advisories], startedAt);
|
|
193
219
|
}
|
|
194
220
|
|
|
195
221
|
if (import.meta.main) await main();
|
package/core/arch-comments.ts
CHANGED
|
@@ -15,6 +15,8 @@ function collectComments(file: string, content: string): CommentSpan[] {
|
|
|
15
15
|
const scanner = ts.createScanner(ts.ScriptTarget.Latest, false, variant, content);
|
|
16
16
|
const starts = lineStarts(content);
|
|
17
17
|
const spans: CommentSpan[] = [];
|
|
18
|
+
const substitutionDepths: number[] = [];
|
|
19
|
+
let braceDepth = 0;
|
|
18
20
|
let token = scanner.scan();
|
|
19
21
|
while (token !== ts.SyntaxKind.EndOfFileToken) {
|
|
20
22
|
if (
|
|
@@ -28,6 +30,18 @@ function collectComments(file: string, content: string): CommentSpan[] {
|
|
|
28
30
|
endLine: lineAt(starts, Math.max(start, end - 1)),
|
|
29
31
|
isDoc: isDocComment(content.slice(start, end)),
|
|
30
32
|
});
|
|
33
|
+
} else if (token === ts.SyntaxKind.TemplateHead) {
|
|
34
|
+
substitutionDepths.push(braceDepth);
|
|
35
|
+
} else if (token === ts.SyntaxKind.OpenBraceToken) {
|
|
36
|
+
braceDepth += 1;
|
|
37
|
+
} else if (token === ts.SyntaxKind.CloseBraceToken) {
|
|
38
|
+
if (substitutionDepths.at(-1) === braceDepth) {
|
|
39
|
+
if (scanner.reScanTemplateToken(false) === ts.SyntaxKind.TemplateTail) {
|
|
40
|
+
substitutionDepths.pop();
|
|
41
|
+
}
|
|
42
|
+
} else {
|
|
43
|
+
braceDepth -= 1;
|
|
44
|
+
}
|
|
31
45
|
}
|
|
32
46
|
token = scanner.scan();
|
|
33
47
|
}
|
package/core/config.schema.ts
CHANGED
|
@@ -244,7 +244,7 @@ const CountDocCommentsSchema = z
|
|
|
244
244
|
.boolean()
|
|
245
245
|
.optional()
|
|
246
246
|
.describe(
|
|
247
|
-
"Count doc-comments toward the limit. Doc-comments are `/** … */` JSDoc blocks (TS/JS) and
|
|
247
|
+
"Count doc-comments toward the limit. Doc-comments are `/** … */` JSDoc blocks (TS/JS), `///` (Swift), and `///` or `//!` (Rust); ordinary `//`, `/* */`, and `#` comments are always counted. Defaults to false, so documentation is exempt and only explanatory/inline comments are capped. Has no effect on Python: docstrings are string expressions rather than comments, so they never count toward either limit."
|
|
248
248
|
);
|
|
249
249
|
|
|
250
250
|
const CommentIgnoreSchema = StringOrStringArray.optional().describe(
|
|
@@ -377,7 +377,7 @@ export const KlintConfigSchema = z
|
|
|
377
377
|
.string()
|
|
378
378
|
.optional()
|
|
379
379
|
.describe(
|
|
380
|
-
"JSON Schema reference. Use ./klint.schema.json for local validation or https://klint.
|
|
380
|
+
"JSON Schema reference. Use ./klint.schema.json for local validation, or a version-tagged URL such as https://raw.githubusercontent.com/konvert7/klint/refs/tags/v0.29.0/klint.schema.json to pin the schema. A tagged URL naming a version other than the installed one raises a klint/schema-version warning."
|
|
381
381
|
),
|
|
382
382
|
root: z
|
|
383
383
|
.string()
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Violation } from "./types";
|
|
2
|
+
|
|
3
|
+
const SCHEMA_VERSION_RULE = "klint/schema-version";
|
|
4
|
+
|
|
5
|
+
const TAGGED_SCHEMA_URL = /\/refs\/tags\/v(\d+\.\d+\.\d+(?:-[0-9A-Za-z.]+)?)\//;
|
|
6
|
+
const UNRELEASED = "0.0.0";
|
|
7
|
+
|
|
8
|
+
export function schemaUrlForVersion(version: string): string {
|
|
9
|
+
return `https://raw.githubusercontent.com/konvert7/klint/refs/tags/v${version}/klint.schema.json`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function declaredSchemaVersion(schema: string | undefined): string | undefined {
|
|
13
|
+
return schema?.match(TAGGED_SCHEMA_URL)?.[1];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function schemaVersionAdvisory({
|
|
17
|
+
schema,
|
|
18
|
+
installed,
|
|
19
|
+
configFile,
|
|
20
|
+
configText,
|
|
21
|
+
}: {
|
|
22
|
+
schema: string | undefined;
|
|
23
|
+
installed: string;
|
|
24
|
+
configFile: string;
|
|
25
|
+
configText: string;
|
|
26
|
+
}): Array<Omit<Violation, "fix">> {
|
|
27
|
+
const declared = declaredSchemaVersion(schema);
|
|
28
|
+
if (declared === undefined || declared === installed || installed === UNRELEASED) {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return [
|
|
33
|
+
{
|
|
34
|
+
file: configFile,
|
|
35
|
+
line: schemaDeclarationLine(configText),
|
|
36
|
+
rule: SCHEMA_VERSION_RULE,
|
|
37
|
+
severity: "warn",
|
|
38
|
+
message: `config declares schema v${declared} but klint ${installed} is installed — update $schema to ${schemaUrlForVersion(installed)}`,
|
|
39
|
+
},
|
|
40
|
+
];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function schemaDeclarationLine(configText: string): number {
|
|
44
|
+
const index = configText.split("\n").findIndex((line) => line.includes("$schema"));
|
|
45
|
+
return index === -1 ? 1 : index + 1;
|
|
46
|
+
}
|
package/core/types.ts
CHANGED
|
@@ -108,7 +108,7 @@ interface ArchMaxLinesRule {
|
|
|
108
108
|
interface ArchCommentRuleBase {
|
|
109
109
|
limit: number;
|
|
110
110
|
in: string | string[];
|
|
111
|
-
/** Count doc-comments (`/** */` JSDoc,
|
|
111
|
+
/** Count doc-comments (`/** */` JSDoc, Swift `///`, Rust `///` and `//!`) toward the limit. Defaults to false. No effect on Python, whose docstrings are never counted. */
|
|
112
112
|
countDocComments?: boolean;
|
|
113
113
|
/** Comment lines matching these patterns count toward neither limit. Literal substring, or `re:` for a regex. */
|
|
114
114
|
ignore?: string | string[];
|