@konvert7/klint 0.38.0 → 1.0.1

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
package/core/arch.ts DELETED
@@ -1,470 +0,0 @@
1
- import { dirname, isAbsolute, resolve } from "node:path";
2
- import ts from "typescript";
3
- import {
4
- commentLineSet,
5
- firstCommentBlockOverrun,
6
- ignoredCommentLines,
7
- } from "./arch-comments";
8
- import { walkAst } from "./ast";
9
- import { relativeSlashPath, toSlashPath } from "./paths";
10
- import type { ArchConfig, Severity, Violation } from "./types";
11
-
12
- interface AliasEntry {
13
- /** The prefix to match (pattern with `/*` stripped, e.g. `"@"` from `"@/*"`). */
14
- prefix: string;
15
- /** Resolved absolute base directory (target with `/*` stripped). */
16
- base: string;
17
- isWildcard: boolean;
18
- }
19
-
20
- function loadPathAliases(root: string): AliasEntry[] {
21
- const tsconfigPath = resolve(root, "tsconfig.json");
22
- const parsed = ts.getParsedCommandLineOfConfigFile(tsconfigPath, undefined, {
23
- ...ts.sys,
24
- onUnRecoverableConfigFileDiagnostic: () => {},
25
- });
26
- if (!parsed) return [];
27
- const { paths, baseUrl } = parsed.options;
28
- if (!paths) return [];
29
- // baseUrl is absolute when set; fall back to tsconfig dir for TS 5+ pathless baseUrl
30
- const base = baseUrl ?? root;
31
- const entries: AliasEntry[] = [];
32
- for (const [pattern, targets] of Object.entries(paths)) {
33
- if (targets.length === 0) continue;
34
- const isWildcard = pattern.endsWith("/*");
35
- const prefix = isWildcard ? pattern.slice(0, -2) : pattern;
36
- const targetStr = targets[0];
37
- const targetBase = targetStr.endsWith("/*") ? targetStr.slice(0, -2) : targetStr;
38
- entries.push({ prefix, base: toSlashPath(resolve(base, targetBase)), isWildcard });
39
- }
40
- return entries;
41
- }
42
-
43
- function resolveAlias(importPath: string, aliases: AliasEntry[]): string | undefined {
44
- for (const alias of aliases) {
45
- if (alias.isWildcard) {
46
- const matchPrefix = `${alias.prefix}/`;
47
- if (importPath.startsWith(matchPrefix)) {
48
- return toSlashPath(resolve(alias.base, importPath.slice(matchPrefix.length)));
49
- }
50
- } else if (importPath === alias.prefix) {
51
- return alias.base;
52
- }
53
- }
54
- return undefined;
55
- }
56
-
57
- interface ImportRecord {
58
- path: string;
59
- resolved: string;
60
- isTypeOnly: boolean;
61
- line: number;
62
- }
63
-
64
- function isBareSpecifier(path: string): boolean {
65
- return !path.startsWith(".") && !isAbsolute(path);
66
- }
67
-
68
- function globToPrefix(glob: string, root: string): string {
69
- return toSlashPath(resolve(root, glob.split("/**")[0].split("/*")[0].split("*")[0]));
70
- }
71
-
72
- function resolveGlobs(
73
- ref: string | string[],
74
- layers: Record<string, string[]> | undefined
75
- ): string[] {
76
- const items = Array.isArray(ref) ? ref : [ref];
77
- return items.flatMap((item) => layers?.[item] ?? [item]);
78
- }
79
-
80
- function resolveLayerPrefixes(
81
- ref: string | string[],
82
- layers: Record<string, string[]> | undefined,
83
- root: string
84
- ): string[] {
85
- return resolveGlobs(ref, layers)
86
- .filter((g) => !g.startsWith("!"))
87
- .map((g) => globToPrefix(g, root));
88
- }
89
-
90
- function resolveLayerFiles(
91
- ref: string | string[],
92
- layers: Record<string, string[]> | undefined,
93
- root: string,
94
- allFiles: string[]
95
- ): string[] {
96
- const globs = resolveGlobs(ref, layers);
97
- const includePrefixes = globs
98
- .filter((g) => !g.startsWith("!"))
99
- .map((g) => globToPrefix(g, root));
100
- const excludePrefixes = globs
101
- .filter((g) => g.startsWith("!"))
102
- .map((g) => globToPrefix(g.slice(1), root));
103
- return allFiles.filter(
104
- (f) =>
105
- includePrefixes.some((p) => f === p || f.startsWith(`${p}/`)) &&
106
- !excludePrefixes.some((p) => f === p || f.startsWith(`${p}/`))
107
- );
108
- }
109
-
110
- function matchesPackage(specifier: string, entries: string[]): boolean {
111
- return entries.some(
112
- (entry) => specifier === entry || specifier.startsWith(`${entry}/`)
113
- );
114
- }
115
-
116
- function inPrefixes(absPath: string, prefixes: string[]): boolean {
117
- return prefixes.some((p) => {
118
- if (absPath === p || absPath.startsWith(`${p}/`)) return true;
119
- // Extension-less imports ("../cli") won't match a prefix like "{root}/cli.ts"
120
- const bare = p.replace(/\.(tsx?|jsx?|mts|cts)$/, "");
121
- return bare !== p && (absPath === bare || absPath.startsWith(`${bare}/`));
122
- });
123
- }
124
-
125
- function scanImports(
126
- file: string,
127
- content: string,
128
- aliases: AliasEntry[]
129
- ): ImportRecord[] {
130
- const records: ImportRecord[] = [];
131
- const fileDir = dirname(file);
132
-
133
- walkAst(file, content, (node, src) => {
134
- let specifierNode: ts.StringLiteral | undefined;
135
- let isTypeOnly = false;
136
-
137
- if (ts.isImportDeclaration(node)) {
138
- if (ts.isStringLiteral(node.moduleSpecifier)) {
139
- specifierNode = node.moduleSpecifier;
140
- isTypeOnly = node.importClause?.isTypeOnly ?? false;
141
- }
142
- } else if (ts.isExportDeclaration(node)) {
143
- if (node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) {
144
- specifierNode = node.moduleSpecifier;
145
- isTypeOnly = node.isTypeOnly;
146
- }
147
- } else if (
148
- ts.isCallExpression(node) &&
149
- node.expression.kind === ts.SyntaxKind.ImportKeyword &&
150
- node.arguments.length >= 1 &&
151
- ts.isStringLiteral(node.arguments[0])
152
- ) {
153
- specifierNode = node.arguments[0] as ts.StringLiteral;
154
- }
155
-
156
- if (!specifierNode) return;
157
-
158
- const path = specifierNode.text;
159
- let resolved: string;
160
- if (isBareSpecifier(path)) {
161
- resolved = resolveAlias(path, aliases) ?? path;
162
- } else {
163
- resolved = toSlashPath(resolve(fileDir, path));
164
- }
165
- const { line } = src.getLineAndCharacterOfPosition(specifierNode.getStart());
166
- records.push({ path, resolved, isTypeOnly, line: line + 1 });
167
- });
168
-
169
- return records;
170
- }
171
-
172
- export function runArchRules(
173
- arch: ArchConfig,
174
- allFiles: string[],
175
- fileContents: Map<string, string>,
176
- root: string
177
- ): Violation[] {
178
- const violations: Violation[] = [];
179
- const layers = arch.layers;
180
- const aliases = loadPathAliases(root);
181
-
182
- for (const rule of arch.imports ?? []) {
183
- const severity: Severity = rule.severity ?? "error";
184
- const fromFiles = resolveLayerFiles(rule.from, layers, root, allFiles);
185
- const denyPackages = toArray(rule["deny-packages"] ?? []);
186
-
187
- for (const file of fromFiles) {
188
- const content = fileContents.get(file);
189
- if (!content) continue;
190
-
191
- for (const imp of scanImports(file, content, aliases)) {
192
- if (rule["type-only"] === "allow" && imp.isTypeOnly) continue;
193
-
194
- const relFile = relativeSlashPath(root, file);
195
-
196
- if (isBareSpecifier(imp.resolved)) {
197
- if (matchesPackage(imp.path, denyPackages)) {
198
- violations.push({
199
- file: relFile,
200
- line: imp.line,
201
- message: rule.message ?? "Import of a denied package",
202
- rule: "arch/imports",
203
- severity,
204
- });
205
- }
206
- continue;
207
- }
208
-
209
- if (rule.deny !== undefined) {
210
- const denyPrefixes = resolveLayerPrefixes(rule.deny, layers, root);
211
- if (inPrefixes(imp.resolved, denyPrefixes)) {
212
- violations.push({
213
- file: relFile,
214
- line: imp.line,
215
- message: rule.message ?? "Import crosses a denied boundary",
216
- rule: "arch/imports",
217
- severity,
218
- });
219
- }
220
- } else if (rule.allow !== undefined) {
221
- const allowPrefixes = resolveLayerPrefixes(rule.allow, layers, root);
222
- if (!inPrefixes(imp.resolved, allowPrefixes)) {
223
- violations.push({
224
- file: relFile,
225
- line: imp.line,
226
- message: rule.message ?? "Import is not in the allowed list",
227
- rule: "arch/imports",
228
- severity,
229
- });
230
- }
231
- }
232
- }
233
- }
234
- }
235
-
236
- for (const rule of arch.forbidden ?? []) {
237
- const severity: Severity = rule.severity ?? "error";
238
- const inFiles = resolveLayerFiles(rule.in, layers, root, allFiles);
239
- if ("jsx-element" in rule) {
240
- scanJsxElements(
241
- inFiles,
242
- toArray(rule["jsx-element"]),
243
- fileContents,
244
- root,
245
- "arch/forbidden",
246
- rule.message,
247
- severity,
248
- violations
249
- );
250
- } else {
251
- scanLinesForPattern(
252
- inFiles,
253
- rule.pattern,
254
- fileContents,
255
- root,
256
- "arch/forbidden",
257
- rule.message,
258
- severity,
259
- violations
260
- );
261
- }
262
- }
263
-
264
- for (const rule of arch.singleton ?? []) {
265
- const severity: Severity = rule.severity ?? "error";
266
- const onlyFile = toSlashPath(resolve(root, rule.only));
267
- const inFiles = rule.in
268
- ? resolveLayerFiles(rule.in, layers, root, allFiles)
269
- : allFiles;
270
- const scope = inFiles.filter((f) => f !== onlyFile);
271
- if ("jsx-element" in rule) {
272
- scanJsxElements(
273
- scope,
274
- toArray(rule["jsx-element"]),
275
- fileContents,
276
- root,
277
- "arch/singleton",
278
- rule.message,
279
- severity,
280
- violations
281
- );
282
- } else {
283
- scanLinesForPattern(
284
- scope,
285
- rule.pattern,
286
- fileContents,
287
- root,
288
- "arch/singleton",
289
- rule.message,
290
- severity,
291
- violations
292
- );
293
- }
294
- }
295
-
296
- for (const rule of arch.maxLines ?? []) {
297
- const severity: Severity = rule.severity ?? "error";
298
- const inFiles = resolveLayerFiles(rule.in, layers, root, allFiles);
299
- for (const file of inFiles) {
300
- const content = fileContents.get(file);
301
- if (content === undefined) continue;
302
- if (countLines(content) > rule.limit) {
303
- violations.push({
304
- file: relativeSlashPath(root, file),
305
- line: rule.limit + 1,
306
- message: rule.message ?? `File exceeds the maximum of ${rule.limit} lines`,
307
- rule: "arch/max-lines",
308
- severity,
309
- });
310
- }
311
- }
312
- }
313
-
314
- for (const rule of arch.maxCommentDensity ?? []) {
315
- const severity: Severity = rule.severity ?? "error";
316
- const ignoreMatcher = buildIgnoreMatcher(rule.ignore);
317
- const inFiles = resolveLayerFiles(rule.in, layers, root, allFiles);
318
- for (const file of inFiles) {
319
- const content = fileContents.get(file);
320
- if (content === undefined) continue;
321
- const total = countLines(content);
322
- if (total === 0) continue;
323
- const commentLines = commentLineSet(file, content, rule.countDocComments ?? false);
324
- const ignored = ignoreMatcher
325
- ? ignoredCommentLines(content, commentLines, ignoreMatcher)
326
- : undefined;
327
- const counted = commentLines.size - (ignored?.size ?? 0);
328
- const density = (counted / total) * 100;
329
- if (density > rule.limit) {
330
- violations.push({
331
- file: relativeSlashPath(root, file),
332
- line: 1,
333
- message:
334
- rule.message ??
335
- `Comment density ${density.toFixed(1)}% exceeds the maximum of ${rule.limit}%`,
336
- rule: "arch/max-comment-density",
337
- severity,
338
- });
339
- }
340
- }
341
- }
342
-
343
- for (const rule of arch.maxCommentBlock ?? []) {
344
- const severity: Severity = rule.severity ?? "error";
345
- const ignoreMatcher = buildIgnoreMatcher(rule.ignore);
346
- const inFiles = resolveLayerFiles(rule.in, layers, root, allFiles);
347
- for (const file of inFiles) {
348
- const content = fileContents.get(file);
349
- if (content === undefined) continue;
350
- const commentLines = [
351
- ...commentLineSet(file, content, rule.countDocComments ?? false),
352
- ].sort((a, b) => a - b);
353
- const ignored = ignoreMatcher
354
- ? ignoredCommentLines(content, commentLines, ignoreMatcher)
355
- : undefined;
356
- const overrun = firstCommentBlockOverrun(commentLines, rule.limit, ignored);
357
- if (overrun !== undefined) {
358
- violations.push({
359
- file: relativeSlashPath(root, file),
360
- line: overrun,
361
- message:
362
- rule.message ??
363
- `Comment block exceeds the maximum of ${rule.limit} consecutive lines`,
364
- rule: "arch/max-comment-block",
365
- severity,
366
- });
367
- }
368
- }
369
- }
370
-
371
- return violations;
372
- }
373
-
374
- /** Counts physical lines the same way Rust's `str::lines()` does — a trailing newline does not add a line. */
375
- function countLines(content: string): number {
376
- if (content.length === 0) return 0;
377
- return content.split("\n").length - (content.endsWith("\n") ? 1 : 0);
378
- }
379
-
380
- function toArray<T>(v: T | T[]): T[] {
381
- return Array.isArray(v) ? v : [v];
382
- }
383
-
384
- function scanJsxElements(
385
- files: string[],
386
- tagNames: string[],
387
- fileContents: Map<string, string>,
388
- root: string,
389
- ruleName: string,
390
- message: string,
391
- severity: Severity,
392
- violations: Violation[]
393
- ): void {
394
- const targets = new Set(tagNames);
395
- for (const file of files) {
396
- if (!/\.(tsx|jsx)$/.test(file)) continue;
397
- const content = fileContents.get(file);
398
- if (!content) continue;
399
- walkAst(file, content, (node, src) => {
400
- if (!ts.isJsxOpeningElement(node) && !ts.isJsxSelfClosingElement(node)) return;
401
- const tagName = node.tagName;
402
- if (!ts.isIdentifier(tagName)) return;
403
- if (!targets.has(tagName.text)) return;
404
- const { line } = src.getLineAndCharacterOfPosition(tagName.getStart());
405
- violations.push({
406
- file: relativeSlashPath(root, file),
407
- line: line + 1,
408
- message,
409
- rule: ruleName,
410
- severity,
411
- });
412
- });
413
- }
414
- }
415
-
416
- const REGEX_PREFIX = "re:";
417
-
418
- function buildIgnoreMatcher(
419
- ignore: string | string[] | undefined
420
- ): ((line: string) => boolean) | undefined {
421
- const patterns = toArray(ignore ?? []);
422
- if (patterns.length === 0) return undefined;
423
- const matchers = patterns.map(buildLineMatcher);
424
- return (line) => matchers.some((matches) => matches(line));
425
- }
426
-
427
- function buildLineMatcher(pattern: string): (line: string) => boolean {
428
- if (!pattern.startsWith(REGEX_PREFIX)) {
429
- return (line) => line.includes(pattern);
430
- }
431
- const source = pattern.slice(REGEX_PREFIX.length);
432
- let regex: RegExp;
433
- try {
434
- regex = new RegExp(source);
435
- } catch (cause) {
436
- throw new Error(
437
- `klint: invalid regex in arch pattern "${pattern}": ${(cause as Error).message}`
438
- );
439
- }
440
- return (line) => regex.test(line);
441
- }
442
-
443
- function scanLinesForPattern(
444
- files: string[],
445
- pattern: string,
446
- fileContents: Map<string, string>,
447
- root: string,
448
- ruleName: string,
449
- message: string,
450
- severity: Severity,
451
- violations: Violation[]
452
- ): void {
453
- const matchesLine = buildLineMatcher(pattern);
454
- for (const file of files) {
455
- const content = fileContents.get(file);
456
- if (!content) continue;
457
- const lines = content.split("\n");
458
- for (let i = 0; i < lines.length; i++) {
459
- if (matchesLine(lines[i])) {
460
- violations.push({
461
- file: relativeSlashPath(root, file),
462
- line: i + 1,
463
- message,
464
- rule: ruleName,
465
- severity,
466
- });
467
- }
468
- }
469
- }
470
- }
package/core/ast.ts DELETED
@@ -1,63 +0,0 @@
1
- import ts from "typescript";
2
-
3
- let _program: { key: string; program: ts.Program } | undefined;
4
- let _sourceFiles: Map<string, ts.SourceFile> = new Map();
5
-
6
- export function clearAstCache(): void {
7
- _program = undefined;
8
- _sourceFiles = new Map();
9
- }
10
-
11
- export function createProgram(files: string[], root: string): ts.Program {
12
- const key = `${root}\0${[...files].sort().join("\0")}`;
13
- if (_program?.key === key) return _program.program;
14
- const configPath = ts.findConfigFile(root, ts.sys.fileExists);
15
- let options: ts.CompilerOptions = { target: ts.ScriptTarget.Latest, strict: true };
16
- if (configPath) {
17
- const { config } = ts.readConfigFile(configPath, ts.sys.readFile);
18
- options = ts.parseJsonConfigFileContent(config, ts.sys, root).options;
19
- }
20
- const program = ts.createProgram(files, options);
21
- _program = { key, program };
22
- return program;
23
- }
24
-
25
- export function nearestFunctionIsAsync(node: ts.Node): boolean {
26
- let cur: ts.Node | undefined = node.parent;
27
- while (cur) {
28
- if (
29
- ts.isFunctionDeclaration(cur) ||
30
- ts.isFunctionExpression(cur) ||
31
- ts.isArrowFunction(cur)
32
- ) {
33
- return cur.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) ?? false;
34
- }
35
- cur = cur.parent;
36
- }
37
- return false;
38
- }
39
-
40
- export function isInsideTry(node: ts.Node): boolean {
41
- let cur: ts.Node | undefined = node.parent;
42
- while (cur) {
43
- if (ts.isTryStatement(cur)) return true;
44
- cur = cur.parent;
45
- }
46
- return false;
47
- }
48
-
49
- export function walkAst(
50
- filePath: string,
51
- content: string,
52
- visitor: (node: ts.Node, src: ts.SourceFile) => void
53
- ): void {
54
- const cached = _sourceFiles.get(filePath);
55
- const src =
56
- cached ?? ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
57
- if (!cached) _sourceFiles.set(filePath, src);
58
- function recurse(node: ts.Node): void {
59
- visitor(node, src);
60
- ts.forEachChild(node, recurse);
61
- }
62
- recurse(src);
63
- }