@konvert7/klint 0.38.0 → 1.0.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.
Files changed (56) hide show
  1. package/README.md +49 -166
  2. package/bin/klint.js +47 -0
  3. package/klint.schema.json +1 -1
  4. package/klint.schema.yaml +1 -1
  5. package/package.json +15 -25
  6. package/cli/command.ts +0 -189
  7. package/cli/commands.ts +0 -90
  8. package/cli/help.ts +0 -21
  9. package/cli/install-skill.ts +0 -260
  10. package/cli/output.ts +0 -178
  11. package/cli/paths.ts +0 -20
  12. package/cli/rust-config.ts +0 -145
  13. package/cli/rust-engine.ts +0 -323
  14. package/cli/stats.ts +0 -92
  15. package/cli.ts +0 -214
  16. package/core/arch-comments.ts +0 -125
  17. package/core/arch.ts +0 -470
  18. package/core/ast.ts +0 -63
  19. package/core/config.schema.ts +0 -413
  20. package/core/fixer.ts +0 -47
  21. package/core/native-binary.ts +0 -123
  22. package/core/paths.ts +0 -9
  23. package/core/rule-helpers.ts +0 -76
  24. package/core/runner.ts +0 -196
  25. package/core/schema-version.ts +0 -46
  26. package/core/skill.ts +0 -164
  27. package/core/stats.ts +0 -160
  28. package/core/types.ts +0 -146
  29. package/plugins/index.ts +0 -6
  30. package/plugins/sonar.ts +0 -27
  31. package/rules/index.ts +0 -26
  32. package/rules/no-async-predicate.ts +0 -51
  33. package/rules/no-consecutive-array-push.ts +0 -61
  34. package/rules/no-date-equality.ts +0 -35
  35. package/rules/no-floating-promise.ts +0 -37
  36. package/rules/no-misused-promises.ts +0 -59
  37. package/rules/no-nested-template-literals.ts +0 -47
  38. package/rules/no-object-in-template.ts +0 -95
  39. package/rules/no-optional-chain-on-non-nullable.ts +0 -47
  40. package/rules/no-single-char-class.ts +0 -113
  41. package/rules/no-string-match.ts +0 -63
  42. package/rules/no-sync-in-async.ts +0 -43
  43. package/rules/no-unguarded-json-parse.ts +0 -35
  44. package/rules/prefer-at.ts +0 -46
  45. package/rules/prefer-nullish-coalescing-assign.ts +0 -111
  46. package/rules/prefer-string-raw-regexp.ts +0 -93
  47. package/rules/prefer-string-raw.ts +0 -39
  48. package/rules/prefer-string-replaceall.ts +0 -67
  49. package/skill/klint-rules/SKILL.md +0 -264
  50. package/tools/generate-homebrew-formula.ts +0 -64
  51. package/tools/prepare-native-release.ts +0 -166
  52. package/tools/prepare-nuget-release.ts +0 -62
  53. package/tools/prepare-python-release.ts +0 -76
  54. package/tools/semantic-release-crates.js +0 -96
  55. package/tools/semantic-release-native-npm.js +0 -121
  56. package/tools/semantic-release-root-npm.js +0 -87
@@ -1,413 +0,0 @@
1
- import { z } from "zod";
2
- import { BUILT_IN_PLUGINS } from "../plugins/index";
3
- import { BUILT_IN_RULES } from "../rules/index";
4
- import type { KlintRule, RuleMeta } from "./types";
5
-
6
- const builtInRuleNames = Object.keys(BUILT_IN_RULES) as [string, ...string[]];
7
- const builtInPluginNames = Object.keys(BUILT_IN_PLUGINS) as [string, ...string[]];
8
- const pluginRuleNames = [
9
- ...new Set(Object.values(BUILT_IN_PLUGINS).flatMap((p) => Object.keys(p.rules))),
10
- ] as [string, ...string[]];
11
- const allKnownRuleNames = [...new Set([...builtInRuleNames, ...pluginRuleNames])] as [
12
- string,
13
- ...string[],
14
- ];
15
-
16
- /** Look up a rule's implementation across BUILT_IN_RULES and plugin implementations. */
17
- function findRule(name: string): KlintRule | undefined {
18
- if (BUILT_IN_RULES[name]) return BUILT_IN_RULES[name];
19
- for (const plugin of Object.values(BUILT_IN_PLUGINS)) {
20
- if (plugin.implementations[name]) return plugin.implementations[name];
21
- }
22
- return undefined;
23
- }
24
-
25
- function metaFor(name: string): RuleMeta | undefined {
26
- return findRule(name)?.meta;
27
- }
28
-
29
- const SeveritySchema = z
30
- .enum(["error", "warn", "off"])
31
- .describe(
32
- 'Rule severity. "error" exits with code 2; "warn" reports but exits 0; "off" silences.'
33
- );
34
-
35
- const RuleOptionsSchema = z
36
- .object({
37
- severity: SeveritySchema.optional(),
38
- include: z
39
- .array(z.string())
40
- .optional()
41
- .describe(
42
- 'Glob patterns scoping this rule to a subset of files. Prefix with ! to exclude. Example: ["src/hooks/**", "!src/hooks/scripts/**"]'
43
- ),
44
- })
45
- .strict()
46
- .describe(
47
- 'Rule options object. Omit severity to default to "error". Add include to scope the rule to specific files.'
48
- );
49
-
50
- const RuleValueSchema = z.union([SeveritySchema, RuleOptionsSchema]);
51
-
52
- // ─── arch ─────────────────────────────────────────────────────────────
53
-
54
- const ArchSeveritySchema = z
55
- .enum(["error", "warn"])
56
- .describe(
57
- 'Severity for the arch rule. "error" exits with code 2; "warn" reports but exits 0. (arch rules cannot be "off" — remove the entry instead.)'
58
- );
59
-
60
- const StringOrStringArray = z
61
- .union([z.string(), z.array(z.string())])
62
- .describe(
63
- "Either a single layer name / glob pattern or an array of them. Layer names defined in `arch.layers` are resolved to their glob lists."
64
- );
65
-
66
- const ArchLayersSchema = z
67
- .record(z.string(), z.array(z.string()))
68
- .describe(
69
- "Named architectural zones. Keys are arbitrary layer names; values are glob patterns matching the files in that layer. Other arch rules reference these names instead of repeating globs."
70
- )
71
- .meta({
72
- examples: [
73
- 'layers:\n core: ["src/hooks/lib/**", "src/tools/**"]\n skills: ["assets/skills/**"]\n dao: ["src/dao/**"]',
74
- ],
75
- });
76
-
77
- const ArchImportRuleSchema = z
78
- .object({
79
- from: StringOrStringArray.describe(
80
- "Source — a layer name or glob(s) for the files this rule applies to. Imports originating in `from` are checked against `deny`/`allow`."
81
- ),
82
- deny: StringOrStringArray.optional().describe(
83
- "Layer name(s) or glob(s) the source is not allowed to import from. Mutually exclusive in spirit with `allow` (use one or the other)."
84
- ),
85
- allow: StringOrStringArray.optional().describe(
86
- "Allowlist of layer name(s) / glob(s) the source may import from. Anything not matching `allow` is denied."
87
- ),
88
- "deny-packages": StringOrStringArray.optional().describe(
89
- "Package specifier(s) the source is not allowed to import — npm packages, `node:` builtins, Python pip packages and stdlib modules, Swift system frameworks, external Rust crates, and C# namespaces or NuGet packages, which `deny`/`allow` cannot reach because they never resolve to a project file. Matching is per segment: `/` in TypeScript/JavaScript, so `next` also covers `next/headers` while `nextra` is unaffected; `.` in Python, Swift, and C#, so `os` also covers `os.path` while `oscrypto` is unaffected; and `::` in Rust, so `tokio` also covers `tokio::sync::Mutex` while `tokio_util` is unaffected. Python, Swift, Rust, and C# sources are checked by the Rust engine."
90
- ),
91
- "type-only": z
92
- .literal("allow")
93
- .optional()
94
- .describe(
95
- 'When set to "allow", `import type` statements that would otherwise be denied are permitted. In Python, imports inside an `if TYPE_CHECKING:` block are exempted the same way. Useful for type-only references across layers.'
96
- ),
97
- message: z
98
- .string()
99
- .optional()
100
- .describe(
101
- "Custom error message reported when the rule is violated. Shown in the agent's event stream alongside file:line."
102
- ),
103
- severity: ArchSeveritySchema.optional(),
104
- })
105
- .strict()
106
- .describe(
107
- "An import-boundary rule. Restricts which layers a source can or cannot import from."
108
- )
109
- .meta({
110
- examples: [
111
- 'arch:\n imports:\n - from: skills\n deny: core\n message: "Skills must be self-contained and portable"\n severity: warn',
112
- 'arch:\n imports:\n - from: ["src/dao/**"]\n allow: ["src/dao/**", "src/prisma/**", "src/types/**"]',
113
- ],
114
- });
115
-
116
- const ArchForbiddenPatternRuleSchema = z
117
- .object({
118
- pattern: z
119
- .string()
120
- .describe(
121
- "Pattern to search for, scanned per line. A literal substring by default (e.g. a console logging call). Prefix with `re:` to match a regular expression instead (e.g. `re:\\bp-\\[\\d+px\\]`). Regexes must use the common JS/RE2 subset — no lookaround or backreferences — so the TS and Rust engines agree. Note: a literal pattern that itself begins with `re:` cannot be expressed; such a value is always read as a regex."
122
- ),
123
- in: StringOrStringArray.describe(
124
- "Layer name(s) or glob(s) the pattern is forbidden in. Files outside the scope are not checked."
125
- ),
126
- message: z
127
- .string()
128
- .describe(
129
- "Required error message explaining why the pattern is forbidden. Shown to the agent so it can fix the call site."
130
- ),
131
- severity: ArchSeveritySchema.optional(),
132
- })
133
- .strict();
134
-
135
- const ArchForbiddenJsxRuleSchema = z
136
- .object({
137
- "jsx-element": StringOrStringArray.describe(
138
- "JSX intrinsic element name(s) to forbid (e.g. `button`, `input`, `label`). AST-matched on opening and self-closing tag names — robust to whitespace, attributes, and naming collisions like `<buttonGroup>`."
139
- ),
140
- in: StringOrStringArray.describe(
141
- "Layer name(s) or glob(s) the element is forbidden in. Files outside the scope are not checked."
142
- ),
143
- message: z
144
- .string()
145
- .describe(
146
- "Required error message explaining why the raw element is forbidden. Typically points at the design-system replacement (e.g. `Use <Button /> from @/components/ui/button`)."
147
- ),
148
- severity: ArchSeveritySchema.optional(),
149
- })
150
- .strict();
151
-
152
- const ArchForbiddenRuleSchema = z
153
- .union([ArchForbiddenPatternRuleSchema, ArchForbiddenJsxRuleSchema])
154
- .describe(
155
- "A forbidden-pattern rule. Blocks either a literal substring (`pattern:`) or a JSX element (`jsx-element:`) inside specific layers."
156
- )
157
- .meta({
158
- examples: [
159
- 'arch:\n forbidden:\n - pattern: "debugLog("\n in: core\n message: "Leaks into the agent event stream"',
160
- 'arch:\n forbidden:\n - jsx-element: ["button", "input", "label"]\n in: ["src/app/**/*.tsx", "src/components/**/*.tsx", "!src/components/ui/**"]\n message: "Use the design-system primitives in @/components/ui/* instead of raw HTML elements."',
161
- ],
162
- });
163
-
164
- const ArchSingletonPatternRuleSchema = z
165
- .object({
166
- pattern: z
167
- .string()
168
- .describe(
169
- "Pattern whose appearance is allowed only at one location, scanned per line. A literal substring by default (e.g. `process.env.API_KEY`). Prefix with `re:` to match a regular expression instead. Regexes must use the common JS/RE2 subset — no lookaround or backreferences — so the TS and Rust engines agree. Note: a literal pattern that itself begins with `re:` cannot be expressed; such a value is always read as a regex."
170
- ),
171
- only: z
172
- .string()
173
- .describe(
174
- "The single file path where this pattern is allowed. Every other occurrence becomes a violation."
175
- ),
176
- in: StringOrStringArray.optional().describe(
177
- "Optional scope. Limit the scan to these layers/globs; defaults to all files in `include`."
178
- ),
179
- message: z.string(),
180
- severity: ArchSeveritySchema.optional(),
181
- })
182
- .strict();
183
-
184
- const ArchSingletonJsxRuleSchema = z
185
- .object({
186
- "jsx-element": StringOrStringArray.describe(
187
- "JSX intrinsic element name(s) allowed only inside the `only` file. AST-matched on tag names."
188
- ),
189
- only: z
190
- .string()
191
- .describe(
192
- "The single file path where the element is allowed (typically the design-system primitive that wraps it)."
193
- ),
194
- in: StringOrStringArray.optional(),
195
- message: z.string(),
196
- severity: ArchSeveritySchema.optional(),
197
- })
198
- .strict();
199
-
200
- const ArchSingletonRuleSchema = z
201
- .union([ArchSingletonPatternRuleSchema, ArchSingletonJsxRuleSchema])
202
- .describe(
203
- "A singleton-location rule. Pins a pattern or JSX element to exactly one file — the only honest way to enforce a module of record or a design-system primitive."
204
- )
205
- .meta({
206
- examples: [
207
- 'arch:\n singleton:\n - pattern: "process.env.API_KEY"\n only: "src/lib/auth.ts"\n message: "API key access must funnel through the auth module."',
208
- 'arch:\n singleton:\n - jsx-element: "button"\n only: "src/components/ui/button.tsx"\n in: ["src/**/*.tsx"]\n message: "Raw <button> belongs only to the Button primitive."',
209
- ],
210
- });
211
-
212
- const ArchMaxLinesRuleSchema = z
213
- .object({
214
- limit: z
215
- .number()
216
- .int()
217
- .positive()
218
- .describe(
219
- "Maximum number of physical lines a file may contain. Counts every line including blanks and comments. A file with more lines than this is a violation."
220
- ),
221
- in: StringOrStringArray.describe(
222
- "Layer name(s) or glob(s) the limit applies to. Files outside the scope are not checked."
223
- ),
224
- message: z
225
- .string()
226
- .optional()
227
- .describe(
228
- "Optional error message. Defaults to a message naming the limit (e.g. 'File exceeds the maximum of 300 lines')."
229
- ),
230
- severity: ArchSeveritySchema.optional(),
231
- })
232
- .strict()
233
- .describe(
234
- "A per-file line-count limit. Caps how long any file in scope may grow — the honest way to keep modules small without a formatter dependency."
235
- )
236
- .meta({
237
- examples: [
238
- 'arch:\n maxLines:\n - limit: 300\n in: src/**\n message: "Split this module"',
239
- "arch:\n maxLines:\n - limit: 300\n in: src/**\n - limit: 600\n in: tests/**",
240
- ],
241
- });
242
-
243
- const CountDocCommentsSchema = z
244
- .boolean()
245
- .optional()
246
- .describe(
247
- "Count doc-comments toward the limit. Doc-comments are `/** … */` JSDoc blocks (TS/JS), `///` (Swift and C#), 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
- );
249
-
250
- const CommentIgnoreSchema = StringOrStringArray.optional().describe(
251
- "Comment lines matching these patterns are structural, not prose — marker comments a build or codegen step reads, for example — and count toward neither limit. A literal substring by default; prefix with `re:` for a regex. Ignored lines still count in the density denominator (they are physical lines) and still hold a comment block together, so a marker cannot be used to split an over-tall block."
252
- );
253
-
254
- const ArchMaxCommentDensityRuleSchema = z
255
- .object({
256
- limit: z
257
- .number()
258
- .positive()
259
- .max(100)
260
- .describe(
261
- "Maximum percentage of a file's physical lines that may be comment lines. Denominator is total physical lines (code + comments + blanks), the same count `maxLines` uses. A file above this ratio is a violation."
262
- ),
263
- countDocComments: CountDocCommentsSchema,
264
- ignore: CommentIgnoreSchema,
265
- in: StringOrStringArray.describe(
266
- "Layer name(s) or glob(s) the limit applies to. Files outside the scope are not checked."
267
- ),
268
- message: z
269
- .string()
270
- .optional()
271
- .describe(
272
- "Optional error message. Defaults to a message naming the measured density and the limit."
273
- ),
274
- severity: ArchSeveritySchema.optional(),
275
- })
276
- .strict()
277
- .describe(
278
- "A per-file comment-density ceiling. Caps how much of a file may be explanatory comments — the honest way to push intent into code and names rather than prose (and to keep token cost down), while leaving doc-comments free by default."
279
- )
280
- .meta({
281
- examples: [
282
- 'arch:\n maxCommentDensity:\n - limit: 5\n in: src/**\n message: "Encode intent in names, not comments"',
283
- "arch:\n maxCommentDensity:\n - limit: 10\n countDocComments: true\n in: src/**",
284
- ],
285
- });
286
-
287
- const ArchMaxCommentBlockRuleSchema = z
288
- .object({
289
- limit: z
290
- .number()
291
- .int()
292
- .positive()
293
- .describe(
294
- "Maximum number of consecutive comment lines allowed in a single block. A run of more comment lines than this — e.g. a tall comment stacked above a statement — is a violation, reported at the first offending line."
295
- ),
296
- countDocComments: CountDocCommentsSchema,
297
- ignore: CommentIgnoreSchema,
298
- in: StringOrStringArray.describe(
299
- "Layer name(s) or glob(s) the limit applies to. Files outside the scope are not checked."
300
- ),
301
- message: z
302
- .string()
303
- .optional()
304
- .describe(
305
- "Optional error message. Defaults to a message naming the consecutive-line limit."
306
- ),
307
- severity: ArchSeveritySchema.optional(),
308
- })
309
- .strict()
310
- .describe(
311
- "A per-block comment-height cap. Limits how many comment lines may stack together, so comments stay terse and a long explanation becomes a well-named function instead. Doc-comments are exempt by default."
312
- )
313
- .meta({
314
- examples: [
315
- 'arch:\n maxCommentBlock:\n - limit: 2\n in: src/**\n message: "Extract a named function instead of a comment block"',
316
- ],
317
- });
318
-
319
- const ArchSchema = z
320
- .object({
321
- layers: ArchLayersSchema.optional(),
322
- imports: z
323
- .array(ArchImportRuleSchema)
324
- .optional()
325
- .describe(
326
- "Import-boundary rules. The dependency graph your README claims you have."
327
- ),
328
- forbidden: z
329
- .array(ArchForbiddenRuleSchema)
330
- .optional()
331
- .describe("Forbidden-pattern rules. Block literal strings scoped to a layer."),
332
- singleton: z
333
- .array(ArchSingletonRuleSchema)
334
- .optional()
335
- .describe(
336
- "Singleton-location rules. Pin a pattern to one file; every other touch is a violation."
337
- ),
338
- maxLines: z
339
- .array(ArchMaxLinesRuleSchema)
340
- .optional()
341
- .describe(
342
- "Per-file line-count limits. Cap how long files in a layer may grow, with different ceilings per scope."
343
- ),
344
- maxCommentDensity: z
345
- .array(ArchMaxCommentDensityRuleSchema)
346
- .optional()
347
- .describe(
348
- "Per-file comment-density ceilings. Cap the share of a file that may be explanatory comments; doc-comments are exempt by default."
349
- ),
350
- maxCommentBlock: z
351
- .array(ArchMaxCommentBlockRuleSchema)
352
- .optional()
353
- .describe(
354
- "Per-block comment-height caps. Limit how many comment lines may stack together; doc-comments are exempt by default."
355
- ),
356
- })
357
- .strict()
358
- .describe(
359
- "Architecture-as-Code constraints — layers, import boundaries, forbidden patterns, singleton locations, comment budgets. Enforced by the arch engine alongside the rule set."
360
- );
361
-
362
- function buildRulesSchema() {
363
- const shape: Record<string, z.ZodType> = {};
364
- for (const name of allKnownRuleNames) {
365
- const meta = metaFor(name);
366
- let entry: z.ZodType = RuleValueSchema.optional();
367
- if (meta?.description) entry = entry.describe(meta.description);
368
- if (meta?.examples?.length) entry = entry.meta({ examples: meta.examples });
369
- shape[name] = entry;
370
- }
371
- return z.object(shape).catchall(RuleValueSchema);
372
- }
373
-
374
- export const KlintConfigSchema = z
375
- .object({
376
- $schema: z
377
- .string()
378
- .optional()
379
- .describe(
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
- ),
382
- root: z
383
- .string()
384
- .optional()
385
- .describe(
386
- "Root directory used to resolve include paths and report relative file names. Defaults to the directory containing klint.config.json."
387
- ),
388
- include: z
389
- .array(z.string())
390
- .optional()
391
- .describe(
392
- 'Glob patterns selecting which TypeScript files to lint. Prefix with ! to exclude. Defaults to ["."] which lints all .ts files under root. Example: ["src", "klint", "!**/node_modules/**"]'
393
- ),
394
- plugins: z
395
- .array(z.enum(builtInPluginNames))
396
- .optional()
397
- .describe(
398
- 'Named rule bundles to enable. Each plugin applies a default set of rules at "error" severity. Individual rules from the bundle can be overridden or silenced via the rules map. Available: "sonar".'
399
- ),
400
- rules: buildRulesSchema()
401
- .optional()
402
- .describe(
403
- 'Map of rule name → severity or options. Example: { "no-floating-promise": "error", "no-sync-in-async": { "severity": "warn", "include": ["src/hooks/**"] } }.'
404
- ),
405
- arch: ArchSchema.optional(),
406
- })
407
- .strict()
408
- .describe(
409
- "klint configuration. Lives at klint.yaml (or klint.config.json) next to biome.json and knip.json."
410
- );
411
-
412
- /** @lintignore */
413
- export type KlintConfigFile = z.infer<typeof KlintConfigSchema>;
package/core/fixer.ts DELETED
@@ -1,47 +0,0 @@
1
- import { readFileSync, writeFileSync } from "node:fs";
2
- import { resolve } from "node:path";
3
- import type { Violation } from "./types";
4
-
5
- /**
6
- * Applies all fixable violations to their source files.
7
- * Fixes are applied bottom-to-top within each file to preserve line offsets.
8
- * Returns the number of fixes applied.
9
- */
10
- export function applyFixes(violations: Violation[], root: string): number {
11
- const byFile = new Map<string, Violation[]>();
12
- for (const v of violations) {
13
- if (!v.fix) continue;
14
- const abs = resolve(root, v.file);
15
- const existing = byFile.get(abs) ?? [];
16
- existing.push(v);
17
- byFile.set(abs, existing);
18
- }
19
-
20
- let applied = 0;
21
- for (const [absPath, fileViolations] of byFile) {
22
- const lines = readFileSync(absPath, "utf-8").split("\n");
23
- const sorted = [...fileViolations].sort(byLargestFixRangeFirst);
24
-
25
- const used: Array<{ start: number; end: number }> = [];
26
- for (const v of sorted) {
27
- if (!v.fix) continue;
28
- const { startLine, endLine, replacement } = v.fix;
29
- if (used.some((r) => startLine <= r.end && endLine >= r.start)) continue;
30
- lines.splice(startLine - 1, endLine - startLine + 1, ...replacement.split("\n"));
31
- used.push({ start: startLine, end: endLine });
32
- applied++;
33
- }
34
-
35
- writeFileSync(absPath, lines.join("\n"), "utf-8");
36
- }
37
-
38
- return applied;
39
- }
40
-
41
- /**
42
- * Largest-range fix wins: sorting by `endLine` descending applies fixes bottom-to-top
43
- * and lets an outer or overlapping fix beat inner ones that share a start position.
44
- */
45
- function byLargestFixRangeFirst(a: Violation, b: Violation): number {
46
- return (b.fix?.endLine ?? 0) - (a.fix?.endLine ?? 0);
47
- }
@@ -1,123 +0,0 @@
1
- import { existsSync, readFileSync } from "node:fs";
2
- import { createRequire } from "node:module";
3
- import { dirname, join } from "node:path";
4
-
5
- type NativePlatform = "darwin" | "linux" | "win32";
6
- type NativeArch = "arm64" | "x64";
7
-
8
- export interface NativePackage {
9
- platform: NativePlatform;
10
- arch: NativeArch;
11
- packageName: string;
12
- binaryPath: string;
13
- }
14
-
15
- const NATIVE_PACKAGES: Record<string, NativePackage> = {
16
- "darwin-arm64": {
17
- platform: "darwin",
18
- arch: "arm64",
19
- packageName: "@konvert7/klint-darwin-arm64",
20
- binaryPath: "bin/klint-rs",
21
- },
22
- "darwin-x64": {
23
- platform: "darwin",
24
- arch: "x64",
25
- packageName: "@konvert7/klint-darwin-x64",
26
- binaryPath: "bin/klint-rs",
27
- },
28
- "linux-x64": {
29
- platform: "linux",
30
- arch: "x64",
31
- packageName: "@konvert7/klint-linux-x64",
32
- binaryPath: "bin/klint-rs",
33
- },
34
- "win32-x64": {
35
- platform: "win32",
36
- arch: "x64",
37
- packageName: "@konvert7/klint-win32-x64",
38
- binaryPath: "bin/klint-rs.exe",
39
- },
40
- };
41
-
42
- interface NativeBinaryOptions {
43
- packageRoot: string;
44
- platform?: string;
45
- arch?: string;
46
- exists?: (path: string) => boolean;
47
- readPackageJson?: (path: string) => { version?: string };
48
- resolvePackageJson?: (packageName: string) => string;
49
- }
50
-
51
- export function nativePackageForPlatform(
52
- platform: string = process.platform,
53
- arch: string = process.arch
54
- ): NativePackage | undefined {
55
- return NATIVE_PACKAGES[`${platform}-${arch}`];
56
- }
57
-
58
- export function nativePackages(): NativePackage[] {
59
- return Object.values(NATIVE_PACKAGES);
60
- }
61
-
62
- export function resolveNativePackageBinary({
63
- packageRoot,
64
- platform = process.platform,
65
- arch = process.arch,
66
- exists = existsSync,
67
- readPackageJson = readJsonFile,
68
- resolvePackageJson,
69
- }: NativeBinaryOptions): string | undefined {
70
- const nativePackage = nativePackageForPlatform(platform, arch);
71
- if (!nativePackage) return undefined;
72
-
73
- const packageJsonPath = resolveOptionalPackageJson(
74
- packageRoot,
75
- nativePackage.packageName,
76
- resolvePackageJson
77
- );
78
- if (!packageJsonPath) return undefined;
79
- if (!nativePackageVersionMatches(packageRoot, packageJsonPath, readPackageJson)) {
80
- return undefined;
81
- }
82
-
83
- const binaryPath = join(dirname(packageJsonPath), nativePackage.binaryPath);
84
- return exists(binaryPath) ? binaryPath : undefined;
85
- }
86
-
87
- function nativePackageVersionMatches(
88
- packageRoot: string,
89
- nativePackageJsonPath: string,
90
- readPackageJson: (path: string) => { version?: string }
91
- ): boolean {
92
- try {
93
- const rootVersion = readPackageJson(join(packageRoot, "package.json")).version;
94
- const nativeVersion = readPackageJson(nativePackageJsonPath).version;
95
- if (!rootVersion || !nativeVersion) return true;
96
- return rootVersion === nativeVersion;
97
- } catch {
98
- return true;
99
- }
100
- }
101
-
102
- function readJsonFile(path: string): { version?: string } {
103
- try {
104
- return JSON.parse(readFileSync(path, "utf-8")) as { version?: string };
105
- } catch {
106
- return {};
107
- }
108
- }
109
-
110
- function resolveOptionalPackageJson(
111
- packageRoot: string,
112
- packageName: string,
113
- resolvePackageJson?: (packageName: string) => string
114
- ): string | undefined {
115
- try {
116
- if (resolvePackageJson) return resolvePackageJson(packageName);
117
- return createRequire(join(packageRoot, "package.json")).resolve(
118
- `${packageName}/package.json`
119
- );
120
- } catch {
121
- return undefined;
122
- }
123
- }
package/core/paths.ts DELETED
@@ -1,9 +0,0 @@
1
- import { relative } from "node:path";
2
-
3
- export function toSlashPath(path: string): string {
4
- return path.replaceAll("\\", "/");
5
- }
6
-
7
- export function relativeSlashPath(from: string, to: string): string {
8
- return toSlashPath(relative(from, to));
9
- }
@@ -1,76 +0,0 @@
1
- import ts from "typescript";
2
- import { createProgram } from "./ast";
3
- import { relativeSlashPath } from "./paths";
4
- import type { KlintRule, RawViolation, RuleMeta } from "./types";
5
- import { defineRule } from "./types";
6
-
7
- export interface AstRuleContext {
8
- sourceFile: ts.SourceFile;
9
- checker: ts.TypeChecker;
10
- root: string;
11
- report(node: ts.Node, message: string): void;
12
- }
13
-
14
- export function defineAstRule(opts: {
15
- meta: RuleMeta;
16
- shouldRun?: (program: ts.Program) => boolean;
17
- visit: (node: ts.Node, ctx: AstRuleContext) => void;
18
- }): KlintRule {
19
- return defineRule({
20
- meta: opts.meta,
21
- check({ files, root }, violations) {
22
- const program = createProgram(files, root);
23
- if (opts.shouldRun && !opts.shouldRun(program)) return;
24
- const checker = program.getTypeChecker();
25
- const fileSet = new Set(files);
26
-
27
- for (const sourceFile of program.getSourceFiles()) {
28
- if (!fileSet.has(sourceFile.fileName) || sourceFile.isDeclarationFile) continue;
29
-
30
- const ctx: AstRuleContext = {
31
- sourceFile,
32
- checker,
33
- root,
34
- report(node, message) {
35
- const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
36
- violations.push({
37
- file: relativeSlashPath(root, sourceFile.fileName),
38
- line: line + 1,
39
- message,
40
- });
41
- },
42
- };
43
-
44
- const walk = (node: ts.Node): void => {
45
- opts.visit(node, ctx);
46
- ts.forEachChild(node, walk);
47
- };
48
- walk(sourceFile);
49
- }
50
- },
51
- });
52
- }
53
-
54
- /** Builds a single-node replacement fix preserving surrounding line text. */
55
- export function buildNodeReplacementFix(
56
- src: ts.SourceFile,
57
- node: ts.Node,
58
- replacement: string
59
- ): NonNullable<RawViolation["fix"]> {
60
- const { line: startLine } = src.getLineAndCharacterOfPosition(node.getStart());
61
- const { line: endLine } = src.getLineAndCharacterOfPosition(node.getEnd());
62
- const lineStarts = src.getLineStarts();
63
- const lineStart = lineStarts[startLine];
64
- const lineEnd =
65
- endLine + 1 < lineStarts.length ? lineStarts[endLine + 1] - 1 : src.text.length;
66
- const linesText = src.text.slice(lineStart, lineEnd);
67
- const nodeOffset = node.getStart() - lineStart;
68
- const nodeEndOffset = node.getEnd() - lineStart;
69
- const fixedLines =
70
- linesText.slice(0, nodeOffset) + replacement + linesText.slice(nodeEndOffset);
71
- return {
72
- startLine: startLine + 1,
73
- endLine: endLine + 1,
74
- replacement: fixedLines,
75
- };
76
- }