@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.
- package/README.md +49 -166
- package/bin/klint.js +47 -0
- package/klint.schema.json +1 -1
- package/klint.schema.yaml +1 -1
- package/package.json +15 -25
- package/cli/command.ts +0 -189
- package/cli/commands.ts +0 -90
- package/cli/help.ts +0 -21
- package/cli/install-skill.ts +0 -260
- package/cli/output.ts +0 -178
- package/cli/paths.ts +0 -20
- package/cli/rust-config.ts +0 -145
- package/cli/rust-engine.ts +0 -323
- package/cli/stats.ts +0 -92
- package/cli.ts +0 -214
- package/core/arch-comments.ts +0 -125
- package/core/arch.ts +0 -470
- package/core/ast.ts +0 -63
- package/core/config.schema.ts +0 -413
- package/core/fixer.ts +0 -47
- package/core/native-binary.ts +0 -123
- package/core/paths.ts +0 -9
- package/core/rule-helpers.ts +0 -76
- package/core/runner.ts +0 -196
- package/core/schema-version.ts +0 -46
- package/core/skill.ts +0 -164
- package/core/stats.ts +0 -160
- package/core/types.ts +0 -146
- package/plugins/index.ts +0 -6
- package/plugins/sonar.ts +0 -27
- package/rules/index.ts +0 -26
- package/rules/no-async-predicate.ts +0 -51
- package/rules/no-consecutive-array-push.ts +0 -61
- package/rules/no-date-equality.ts +0 -35
- package/rules/no-floating-promise.ts +0 -37
- package/rules/no-misused-promises.ts +0 -59
- package/rules/no-nested-template-literals.ts +0 -47
- package/rules/no-object-in-template.ts +0 -95
- package/rules/no-optional-chain-on-non-nullable.ts +0 -47
- package/rules/no-single-char-class.ts +0 -113
- package/rules/no-string-match.ts +0 -63
- package/rules/no-sync-in-async.ts +0 -43
- package/rules/no-unguarded-json-parse.ts +0 -35
- package/rules/prefer-at.ts +0 -46
- package/rules/prefer-nullish-coalescing-assign.ts +0 -111
- package/rules/prefer-string-raw-regexp.ts +0 -93
- package/rules/prefer-string-raw.ts +0 -39
- package/rules/prefer-string-replaceall.ts +0 -67
- package/skill/klint-rules/SKILL.md +0 -264
- package/tools/generate-homebrew-formula.ts +0 -64
- package/tools/prepare-native-release.ts +0 -166
- package/tools/prepare-nuget-release.ts +0 -62
- package/tools/prepare-python-release.ts +0 -76
- package/tools/semantic-release-crates.js +0 -96
- package/tools/semantic-release-native-npm.js +0 -121
- 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
|
-
}
|