@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
package/cli/stats.ts DELETED
@@ -1,92 +0,0 @@
1
- import { resolve } from "node:path";
2
- import { type KlintStats, readStats, STATS_DIR, STATS_FILE } from "../core/stats";
3
- import type { OptionValues } from "./command";
4
-
5
- const RULE_HEADING = "by rule";
6
- const REPORTED_HEADING = "reported";
7
- const PREVENTED_HEADING = "prevented";
8
- const LABEL_WIDTH = "violations reported".length;
9
-
10
- interface RuleRow {
11
- rule: string;
12
- reported: number;
13
- prevented: number;
14
- }
15
-
16
- function ruleRows(stats: KlintStats): RuleRow[] {
17
- const rules = new Set([
18
- ...Object.keys(stats.reported.byRule),
19
- ...Object.keys(stats.prevented.byRule),
20
- ]);
21
- const byCodePoint = (a: string, b: string) => Number(a > b) - Number(a < b);
22
- return [...rules]
23
- .map((rule) => ({
24
- rule,
25
- reported: stats.reported.byRule[rule] ?? 0,
26
- prevented: stats.prevented.byRule[rule] ?? 0,
27
- }))
28
- .sort(
29
- (a, b) =>
30
- b.reported - a.reported ||
31
- b.prevented - a.prevented ||
32
- byCodePoint(a.rule, b.rule)
33
- );
34
- }
35
-
36
- interface Summary {
37
- label: string;
38
- total: number;
39
- detail: string;
40
- }
41
-
42
- function summaryLines(summaries: Summary[]): string[] {
43
- const width = Math.max(...summaries.map(({ total }) => String(total).length));
44
- return summaries.map(
45
- ({ label, total, detail }) =>
46
- ` ${label.padEnd(LABEL_WIDTH)} ${String(total).padStart(width)} (${detail})`
47
- );
48
- }
49
-
50
- function ruleTable(rows: RuleRow[]): string[] {
51
- const width = Math.max(RULE_HEADING.length - 2, ...rows.map(({ rule }) => rule.length));
52
- const heading = ` ${RULE_HEADING.padEnd(width + 2)} ${REPORTED_HEADING} ${PREVENTED_HEADING}`;
53
- const lines = rows.map(
54
- ({ rule, reported, prevented }) =>
55
- ` ${rule.padEnd(width)} ${String(reported).padStart(REPORTED_HEADING.length)} ${String(prevented).padStart(PREVENTED_HEADING.length)}`
56
- );
57
- return [heading, ...lines];
58
- }
59
-
60
- export function formatStats(stats: KlintStats): string {
61
- const { runs, reported, prevented } = stats;
62
- const lines = [
63
- `klint stats (${STATS_DIR}/${STATS_FILE})`,
64
- "",
65
- ...summaryLines([
66
- {
67
- label: "runs",
68
- total: runs.hook + runs.direct,
69
- detail: `hook ${runs.hook}, direct ${runs.direct}`,
70
- },
71
- {
72
- label: "violations reported",
73
- total: reported.bySource.hook + reported.bySource.direct,
74
- detail: `hook ${reported.bySource.hook}, direct ${reported.bySource.direct}`,
75
- },
76
- {
77
- label: "errors prevented",
78
- total: prevented.bySource.hook + prevented.bySource.direct,
79
- detail: `caught by hook ${prevented.bySource.hook}, direct ${prevented.bySource.direct}`,
80
- },
81
- ]),
82
- ];
83
- const rows = ruleRows(stats);
84
- if (rows.length > 0) lines.push("", ...ruleTable(rows));
85
- return `${lines.join("\n")}\n`;
86
- }
87
-
88
- export function printStats(values: OptionValues): void {
89
- const configDir =
90
- typeof values.config === "string" ? resolve(values.config) : process.cwd();
91
- process.stdout.write(formatStats(readStats(configDir)));
92
- }
package/cli.ts DELETED
@@ -1,214 +0,0 @@
1
- #!/usr/bin/env bun
2
- import { existsSync } from "node:fs";
3
- import { readFile } from "node:fs/promises";
4
- import { resolve } from "node:path";
5
- import { parse as parseYaml } from "yaml";
6
- import { type OptionValues, runCommand } from "./cli/command";
7
- import { klintCommand } from "./cli/commands";
8
- import { installSkill } from "./cli/install-skill";
9
- import {
10
- formatDuration,
11
- toJsonCliResult,
12
- withAdvisories,
13
- writeDebugEvent,
14
- writeTextOutput,
15
- } from "./cli/output";
16
- import { installedVersion, SHIPPED_SKILL_PATH } from "./cli/paths";
17
- import { runAutoEngine, runCompareEngine, runRustEngine } from "./cli/rust-engine";
18
- import { printStats } from "./cli/stats";
19
- import { applyFixes } from "./core/fixer";
20
- import { runKlint } from "./core/runner";
21
- import { schemaVersionAdvisory } from "./core/schema-version";
22
- import { skillAdvisories } from "./core/skill";
23
- import { runRecorder } from "./core/stats";
24
- import type { ArchConfig, KlintConfig, KlintRule, RuleConfigValue } from "./core/types";
25
-
26
- export async function main(): Promise<number> {
27
- const startedAt = performance.now();
28
- const command = klintCommand({
29
- lint: (values) => lint(values, startedAt),
30
- stats: printStats,
31
- installSkill,
32
- });
33
- return runCommand(command, process.argv.slice(2), ["klint"]);
34
- }
35
-
36
- function optionalPath(value: string | true | undefined): string | undefined {
37
- return typeof value === "string" ? resolve(value) : undefined;
38
- }
39
-
40
- async function lint(values: OptionValues, startedAt: number): Promise<void> {
41
- if (values.version) {
42
- process.stdout.write(`klint ${installedVersion()}\n`);
43
- return;
44
- }
45
- const rulesFile = optionalPath(values.rules);
46
- const fix = values.fix === true;
47
- const json = values.json === true;
48
- const debug = values.debug === true;
49
- const hook = values.hook === true;
50
- const engine =
51
- typeof values.engine === "string" ? values.engine : process.env.KLINT_ENGINE;
52
- const configDir = optionalPath(values.config) ?? process.cwd();
53
- const record = runRecorder(configDir, hook ? "hook" : "direct");
54
-
55
- const yamlPath = resolve(configDir, "klint.yaml");
56
- const jsonPath = resolve(configDir, "klint.config.json");
57
- const usingYaml = existsSync(yamlPath);
58
- const configPath = usingYaml ? yamlPath : jsonPath;
59
-
60
- if (!existsSync(configPath)) {
61
- process.stderr.write(
62
- `klint: no config file found — create klint.yaml (or klint.config.json) at ${configDir}\n`
63
- );
64
- process.exit(1);
65
- }
66
-
67
- interface RawConfig {
68
- $schema?: string;
69
- root?: string;
70
- include?: string[];
71
- plugins?: string[];
72
- rules?: Record<string, RuleConfigValue>;
73
- arch?: unknown;
74
- }
75
- let raw: RawConfig;
76
- let configText = "";
77
- try {
78
- configText = await readFile(configPath, "utf-8");
79
- raw = (usingYaml ? parseYaml(configText) : JSON.parse(configText)) as RawConfig;
80
- } catch {
81
- process.stderr.write(`klint: failed to parse ${configPath}\n`);
82
- process.exit(1);
83
- }
84
- const root = resolve(configDir, raw.root ?? ".");
85
-
86
- const installed = installedVersion();
87
- const advisories = [
88
- ...schemaVersionAdvisory({
89
- schema: raw.$schema,
90
- installed,
91
- configFile: usingYaml ? "klint.yaml" : "klint.config.json",
92
- configText,
93
- }),
94
- ...skillAdvisories({
95
- configDir,
96
- installed,
97
- shippedSkillPath: SHIPPED_SKILL_PATH,
98
- }),
99
- ];
100
-
101
- if (engine === "rust") {
102
- runRustEngine({ advisories, fix, json, raw, record, root, rulesFile, startedAt });
103
- return;
104
- }
105
- if (
106
- engine !== undefined &&
107
- engine !== "ts" &&
108
- engine !== "rust" &&
109
- engine !== "compare" &&
110
- engine !== "auto"
111
- ) {
112
- process.stderr.write(
113
- `klint: unknown engine "${engine}" (expected "ts", "rust", "compare", or "auto")\n`
114
- );
115
- process.exit(1);
116
- }
117
-
118
- let customRules: Record<string, KlintRule> = {};
119
- const defaultRulesPath = resolve(configDir, "klint.rules.ts");
120
- const rulesPath =
121
- rulesFile ?? (existsSync(defaultRulesPath) ? defaultRulesPath : undefined);
122
- if (rulesPath) {
123
- const mod = await import(rulesPath);
124
- customRules = (mod.default ?? {}) as Record<string, KlintRule>;
125
- }
126
-
127
- const customRulesMap: Record<string, RuleConfigValue> = Object.fromEntries(
128
- Object.keys(customRules).map((name) => [name, "error" as const])
129
- );
130
- const allRules: KlintConfig["rules"] = { ...customRulesMap, ...(raw.rules ?? {}) };
131
-
132
- if (engine === "auto") {
133
- runAutoEngine({
134
- advisories,
135
- fix,
136
- json,
137
- raw,
138
- record,
139
- root,
140
- rulesFile,
141
- startedAt,
142
- customRules,
143
- customRulesMap,
144
- });
145
- return;
146
- }
147
-
148
- const violations = runKlint(
149
- {
150
- root,
151
- include: raw.include ?? ["."],
152
- plugins: raw.plugins,
153
- rules: allRules,
154
- arch: raw.arch as ArchConfig | undefined,
155
- },
156
- customRules,
157
- { onDebug: debug ? writeDebugEvent : undefined }
158
- );
159
-
160
- if (engine === "compare") {
161
- runCompareEngine({
162
- advisories,
163
- fix,
164
- json,
165
- raw,
166
- record,
167
- root,
168
- rulesFile,
169
- tsViolations: violations,
170
- });
171
- return;
172
- }
173
-
174
- if (json) {
175
- record(violations);
176
- const tsResult = toJsonCliResult(violations);
177
- process.stdout.write(withAdvisories(tsResult.stdout, advisories));
178
- process.exit(tsResult.status);
179
- }
180
-
181
- if (fix) {
182
- let totalApplied = 0;
183
- let current = violations;
184
- while (true) {
185
- const applied = applyFixes(current, root);
186
- totalApplied += applied;
187
- if (applied === 0) break;
188
- current = runKlint(
189
- {
190
- root,
191
- include: raw.include ?? ["."],
192
- plugins: raw.plugins,
193
- rules: allRules,
194
- arch: raw.arch as ArchConfig | undefined,
195
- },
196
- customRules,
197
- { onDebug: debug ? writeDebugEvent : undefined }
198
- );
199
- if (current.every((v) => !v.fix)) break;
200
- }
201
- record(current);
202
- const unfixed = current.filter((v) => !v.fix).length;
203
- const msg =
204
- unfixed > 0
205
- ? `klint: applied ${totalApplied} fix(es). ${unfixed} violation(s) require manual attention.`
206
- : `klint: applied ${totalApplied} fix(es). No remaining violations.`;
207
- process.stdout.write(`${msg} Finished in ${formatDuration(startedAt)}.\n`);
208
- process.exit(0);
209
- }
210
- record(violations);
211
- writeTextOutput([...violations, ...advisories], startedAt);
212
- }
213
-
214
- if (import.meta.main) process.exitCode = await main();
@@ -1,125 +0,0 @@
1
- import ts from "typescript";
2
-
3
- interface CommentSpan {
4
- startLine: number;
5
- endLine: number;
6
- isDoc: boolean;
7
- }
8
-
9
- /** JS-like sources are lexed for comments; Python/Swift comment rules run in the native engine. */
10
- function collectComments(file: string, content: string): CommentSpan[] {
11
- if (!/\.(tsx?|jsx?|mts|cts)$/.test(file)) return [];
12
- const variant = /\.(tsx|jsx)$/.test(file)
13
- ? ts.LanguageVariant.JSX
14
- : ts.LanguageVariant.Standard;
15
- const scanner = ts.createScanner(ts.ScriptTarget.Latest, false, variant, content);
16
- const starts = lineStarts(content);
17
- const spans: CommentSpan[] = [];
18
- const substitutionDepths: number[] = [];
19
- let braceDepth = 0;
20
- let token = scanner.scan();
21
- while (token !== ts.SyntaxKind.EndOfFileToken) {
22
- if (
23
- token === ts.SyntaxKind.SingleLineCommentTrivia ||
24
- token === ts.SyntaxKind.MultiLineCommentTrivia
25
- ) {
26
- const start = scanner.getTokenStart();
27
- const end = scanner.getTokenEnd();
28
- spans.push({
29
- startLine: lineAt(starts, start),
30
- endLine: lineAt(starts, Math.max(start, end - 1)),
31
- isDoc: isDocComment(content.slice(start, end)),
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
- }
45
- }
46
- token = scanner.scan();
47
- }
48
- return spans;
49
- }
50
-
51
- /** A `/** *​/` JSDoc block, but not the empty `/**​/` comment. Mirrors the Rust engine. */
52
- function isDocComment(text: string): boolean {
53
- return text.startsWith("/**") && text !== "/**/";
54
- }
55
-
56
- export function commentLineSet(
57
- file: string,
58
- content: string,
59
- countDocComments: boolean
60
- ): Set<number> {
61
- const set = new Set<number>();
62
- for (const span of collectComments(file, content)) {
63
- if (!countDocComments && span.isDoc) continue;
64
- for (let line = span.startLine; line <= span.endLine; line++) set.add(line);
65
- }
66
- return set;
67
- }
68
-
69
- /**
70
- * Comment lines whose source text matches an `ignore` pattern. They stay in the
71
- * density denominator and keep a comment run contiguous, but count toward neither.
72
- */
73
- export function ignoredCommentLines(
74
- content: string,
75
- commentLines: Iterable<number>,
76
- matchesIgnore: (text: string) => boolean
77
- ): Set<number> {
78
- const sourceLines = content.split("\n");
79
- const ignored = new Set<number>();
80
- for (const line of commentLines) {
81
- if (matchesIgnore(sourceLines[line - 1] ?? "")) ignored.add(line);
82
- }
83
- return ignored;
84
- }
85
-
86
- /**
87
- * Returns the first line at which a run of consecutive comment lines exceeds
88
- * `limit`. An ignored line neither counts toward the height nor breaks the run.
89
- */
90
- export function firstCommentBlockOverrun(
91
- sortedLines: number[],
92
- limit: number,
93
- ignored?: ReadonlySet<number>
94
- ): number | undefined {
95
- let counted = 0;
96
- let prev: number | undefined;
97
- for (const line of sortedLines) {
98
- if (prev !== undefined && line !== prev + 1) counted = 0;
99
- prev = line;
100
- if (ignored?.has(line)) continue;
101
- counted += 1;
102
- if (counted > limit) return line;
103
- }
104
- return undefined;
105
- }
106
-
107
- function lineStarts(content: string): number[] {
108
- const starts = [0];
109
- for (let i = 0; i < content.length; i++) {
110
- if (content[i] === "\n") starts.push(i + 1);
111
- }
112
- return starts;
113
- }
114
-
115
- /** 1-based line for a character offset, via binary search over line starts. */
116
- function lineAt(starts: number[], offset: number): number {
117
- let lo = 0;
118
- let hi = starts.length - 1;
119
- while (lo < hi) {
120
- const mid = (lo + hi + 1) >> 1;
121
- if (starts[mid] <= offset) lo = mid;
122
- else hi = mid - 1;
123
- }
124
- return lo + 1;
125
- }