@konvert7/klint 0.19.2 → 0.20.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 CHANGED
@@ -141,6 +141,7 @@ Rust currently supports:
141
141
 
142
142
  - `arch` rules: imports, forbidden patterns, singleton locations
143
143
  - `no-unguarded-json-parse`
144
+ - `no-sync-in-async`
144
145
  - `no-nested-template-literals`
145
146
  - `no-consecutive-array-push`
146
147
  - `no-string-match`
package/cli.ts CHANGED
@@ -31,6 +31,7 @@ const RUST_SUPPORTED_TYPESCRIPT_RULES = new Set([
31
31
  "no-consecutive-array-push",
32
32
  "no-nested-template-literals",
33
33
  "no-string-match",
34
+ "no-sync-in-async",
34
35
  "no-unguarded-json-parse",
35
36
  ]);
36
37
 
package/core/arch.ts CHANGED
@@ -1,10 +1,9 @@
1
- import { dirname, isAbsolute, relative, resolve } from "node:path";
1
+ import { dirname, isAbsolute, resolve } from "node:path";
2
2
  import ts from "typescript";
3
3
  import { walkAst } from "./ast";
4
+ import { relativeSlashPath, toSlashPath } from "./paths";
4
5
  import type { ArchConfig, Severity, Violation } from "./types";
5
6
 
6
- const toSlash = (p: string) => p.replaceAll("\\", "/");
7
-
8
7
  interface AliasEntry {
9
8
  /** The prefix to match (pattern with `/*` stripped, e.g. `"@"` from `"@/*"`). */
10
9
  prefix: string;
@@ -31,7 +30,7 @@ function loadPathAliases(root: string): AliasEntry[] {
31
30
  const prefix = isWildcard ? pattern.slice(0, -2) : pattern;
32
31
  const targetStr = targets[0];
33
32
  const targetBase = targetStr.endsWith("/*") ? targetStr.slice(0, -2) : targetStr;
34
- entries.push({ prefix, base: toSlash(resolve(base, targetBase)), isWildcard });
33
+ entries.push({ prefix, base: toSlashPath(resolve(base, targetBase)), isWildcard });
35
34
  }
36
35
  return entries;
37
36
  }
@@ -41,7 +40,7 @@ function resolveAlias(importPath: string, aliases: AliasEntry[]): string | undef
41
40
  if (alias.isWildcard) {
42
41
  const matchPrefix = `${alias.prefix}/`;
43
42
  if (importPath.startsWith(matchPrefix)) {
44
- return toSlash(resolve(alias.base, importPath.slice(matchPrefix.length)));
43
+ return toSlashPath(resolve(alias.base, importPath.slice(matchPrefix.length)));
45
44
  }
46
45
  } else if (importPath === alias.prefix) {
47
46
  return alias.base;
@@ -62,7 +61,7 @@ function isBareSpecifier(path: string): boolean {
62
61
  }
63
62
 
64
63
  function globToPrefix(glob: string, root: string): string {
65
- return toSlash(resolve(root, glob.split("/**")[0].split("/*")[0].split("*")[0]));
64
+ return toSlashPath(resolve(root, glob.split("/**")[0].split("/*")[0].split("*")[0]));
66
65
  }
67
66
 
68
67
  function resolveGlobs(
@@ -145,7 +144,7 @@ function scanImports(
145
144
  if (isBareSpecifier(path)) {
146
145
  resolved = resolveAlias(path, aliases) ?? path;
147
146
  } else {
148
- resolved = toSlash(resolve(fileDir, path));
147
+ resolved = toSlashPath(resolve(fileDir, path));
149
148
  }
150
149
  const { line } = src.getLineAndCharacterOfPosition(specifierNode.getStart());
151
150
  records.push({ path, resolved, isTypeOnly, line: line + 1 });
@@ -176,7 +175,7 @@ export function runArchRules(
176
175
  if (isBareSpecifier(imp.resolved)) continue;
177
176
  if (rule["type-only"] === "allow" && imp.isTypeOnly) continue;
178
177
 
179
- const relFile = relative(root, file).replaceAll("\\", "/");
178
+ const relFile = relativeSlashPath(root, file);
180
179
 
181
180
  if (rule.deny !== undefined) {
182
181
  const denyPrefixes = resolveLayerPrefixes(rule.deny, layers, root);
@@ -235,7 +234,7 @@ export function runArchRules(
235
234
 
236
235
  for (const rule of arch.singleton ?? []) {
237
236
  const severity: Severity = rule.severity ?? "error";
238
- const onlyFile = toSlash(resolve(root, rule.only));
237
+ const onlyFile = toSlashPath(resolve(root, rule.only));
239
238
  const inFiles = rule.in
240
239
  ? resolveLayerFiles(rule.in, layers, root, allFiles)
241
240
  : allFiles;
@@ -294,7 +293,7 @@ function scanJsxElements(
294
293
  if (!targets.has(tagName.text)) return;
295
294
  const { line } = src.getLineAndCharacterOfPosition(tagName.getStart());
296
295
  violations.push({
297
- file: relative(root, file).replaceAll("\\", "/"),
296
+ file: relativeSlashPath(root, file),
298
297
  line: line + 1,
299
298
  message,
300
299
  rule: ruleName,
@@ -321,7 +320,7 @@ function scanLinesForPattern(
321
320
  for (let i = 0; i < lines.length; i++) {
322
321
  if (lines[i].includes(pattern)) {
323
322
  violations.push({
324
- file: relative(root, file).replaceAll("\\", "/"),
323
+ file: relativeSlashPath(root, file),
325
324
  line: i + 1,
326
325
  message,
327
326
  rule: ruleName,
package/core/paths.ts ADDED
@@ -0,0 +1,9 @@
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,6 +1,6 @@
1
- import { relative } from "node:path";
2
1
  import ts from "typescript";
3
2
  import { createProgram } from "./ast";
3
+ import { relativeSlashPath } from "./paths";
4
4
  import type { KlintRule, RawViolation, RuleMeta } from "./types";
5
5
  import { defineRule } from "./types";
6
6
 
@@ -34,7 +34,7 @@ export function defineAstRule(opts: {
34
34
  report(node, message) {
35
35
  const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
36
36
  violations.push({
37
- file: relative(root, sourceFile.fileName),
37
+ file: relativeSlashPath(root, sourceFile.fileName),
38
38
  line: line + 1,
39
39
  message,
40
40
  });
package/core/runner.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  import { readdirSync, readFileSync } from "node:fs";
2
- import { join, relative, resolve } from "node:path";
2
+ import { join, resolve } from "node:path";
3
3
  import { BUILT_IN_PLUGINS } from "../plugins/index";
4
4
  import { BUILT_IN_RULES } from "../rules/index";
5
5
  import { runArchRules } from "./arch";
6
6
  import { clearAstCache } from "./ast";
7
+ import { relativeSlashPath, toSlashPath } from "./paths";
7
8
  import type {
8
9
  KlintConfig,
9
10
  KlintRule,
@@ -29,7 +30,7 @@ function walk(dir: string, root: string, excludes: string[] = []): string[] {
29
30
  const out: string[] = [];
30
31
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
31
32
  const full = join(dir, entry.name);
32
- const rel = relative(root, full).replaceAll("\\", "/");
33
+ const rel = relativeSlashPath(root, full);
33
34
  if (entry.isDirectory()) {
34
35
  if (
35
36
  excludes.some(
@@ -38,8 +39,7 @@ function walk(dir: string, root: string, excludes: string[] = []): string[] {
38
39
  )
39
40
  continue;
40
41
  out.push(...walk(full, root, excludes));
41
- } else if (/\.(tsx?|jsx?|mts|cts)$/.test(entry.name))
42
- out.push(full.replaceAll("\\", "/"));
42
+ } else if (/\.(tsx?|jsx?|mts|cts)$/.test(entry.name)) out.push(toSlashPath(full));
43
43
  }
44
44
  return out;
45
45
  }
@@ -68,8 +68,8 @@ function resolveFiles(
68
68
  }
69
69
 
70
70
  function matchPattern(relPath: string, pattern: string): boolean {
71
- const norm = relPath.replaceAll("\\", "/");
72
- const p = pattern.replaceAll("\\", "/");
71
+ const norm = toSlashPath(relPath);
72
+ const p = toSlashPath(pattern);
73
73
  if (p === "." || p === "**") return true;
74
74
  if (!p.includes("*")) return norm === p || norm.startsWith(`${p}/`);
75
75
  return globToRegExp(p).test(norm);
@@ -107,7 +107,7 @@ function applyPatterns(files: string[], patterns: string[], root: string): strin
107
107
  const includes = patterns.filter((p) => !p.startsWith("!"));
108
108
  const excludes = patterns.filter((p) => p.startsWith("!")).map((p) => p.slice(1));
109
109
  return files.filter((file) => {
110
- const rel = relative(root, file).replaceAll("\\", "/");
110
+ const rel = relativeSlashPath(root, file);
111
111
  const included = includes.length === 0 || includes.some((p) => matchPattern(rel, p));
112
112
  const excluded = excludes.some((p) => matchPattern(rel, p));
113
113
  return included && !excluded;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@konvert7/klint",
3
- "version": "0.19.2",
3
+ "version": "0.20.0",
4
4
  "description": "Architecture-as-Code linter for TypeScript. Enforce layer boundaries, import rules, and singleton patterns in YAML.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -62,10 +62,10 @@
62
62
  "zod": "^4.4.3"
63
63
  },
64
64
  "optionalDependencies": {
65
- "@konvert7/klint-darwin-arm64": "0.19.2",
66
- "@konvert7/klint-darwin-x64": "0.19.2",
67
- "@konvert7/klint-linux-x64": "0.19.2",
68
- "@konvert7/klint-win32-x64": "0.19.2"
65
+ "@konvert7/klint-darwin-arm64": "0.20.0",
66
+ "@konvert7/klint-darwin-x64": "0.20.0",
67
+ "@konvert7/klint-linux-x64": "0.20.0",
68
+ "@konvert7/klint-win32-x64": "0.20.0"
69
69
  },
70
70
  "devDependencies": {
71
71
  "@biomejs/biome": "2.4.7",
@@ -1,6 +1,6 @@
1
- import { relative } from "node:path";
2
1
  import ts from "typescript";
3
2
  import { walkAst } from "../core/ast";
3
+ import { relativeSlashPath } from "../core/paths";
4
4
  import type { KlintRule } from "../core/types";
5
5
 
6
6
  export const noConsecutiveArrayPush: KlintRule = {
@@ -26,7 +26,7 @@ export const noConsecutiveArrayPush: KlintRule = {
26
26
  statements[runStart].getStart()
27
27
  );
28
28
  violations.push({
29
- file: relative(root, file).replaceAll("\\", "/"),
29
+ file: relativeSlashPath(root, file),
30
30
  line: line + 1,
31
31
  message: `${upTo - runStart} consecutive .push() calls on \`${runReceiver}\` — combine into a single .push(a, b, …) call.`,
32
32
  });
@@ -1,6 +1,6 @@
1
- import { relative } from "node:path";
2
1
  import ts from "typescript";
3
2
  import { walkAst } from "../core/ast";
3
+ import { relativeSlashPath } from "../core/paths";
4
4
  import type { KlintRule } from "../core/types";
5
5
 
6
6
  export const noNestedTemplateLiterals: KlintRule = {
@@ -32,7 +32,7 @@ function findNestedTemplate(
32
32
  if (ts.isTemplateExpression(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
33
33
  const { line } = src.getLineAndCharacterOfPosition(node.getStart());
34
34
  violations.push({
35
- file: relative(root, file).replaceAll("\\", "/"),
35
+ file: relativeSlashPath(root, file),
36
36
  line: line + 1,
37
37
  message:
38
38
  "Nested template literal — extract the inner template to a variable to improve readability.",
@@ -87,7 +87,7 @@ function wouldRenderAsObjectObject(type: ts.Type, checker: ts.TypeChecker): bool
87
87
  const fileName = d.getSourceFile().fileName;
88
88
  return (
89
89
  !fileName.includes("/typescript/lib/lib") &&
90
- !fileName.includes("\\typescript\\lib\\lib")
90
+ !fileName.includes(String.raw`\typescript\lib\lib`)
91
91
  );
92
92
  });
93
93
 
@@ -1,6 +1,6 @@
1
- import { relative } from "node:path";
2
1
  import ts from "typescript";
3
2
  import { walkAst } from "../core/ast";
3
+ import { relativeSlashPath } from "../core/paths";
4
4
  import { buildNodeReplacementFix } from "../core/rule-helpers";
5
5
  import type { KlintRule } from "../core/types";
6
6
 
@@ -99,7 +99,7 @@ export const noSingleCharClass: KlintRule = {
99
99
  const fix = buildNodeReplacementFix(src, node, fixedRegex);
100
100
 
101
101
  violations.push({
102
- file: relative(root, file),
102
+ file: relativeSlashPath(root, file),
103
103
  line: fix.startLine,
104
104
  message: `Character class [${toFix[0].inner}] contains a single element — remove the brackets.`,
105
105
  fix,
@@ -1,6 +1,6 @@
1
- import { relative } from "node:path";
2
1
  import ts from "typescript";
3
2
  import { walkAst } from "../core/ast";
3
+ import { relativeSlashPath } from "../core/paths";
4
4
  import type { KlintRule } from "../core/types";
5
5
 
6
6
  export const noStringMatch: KlintRule = {
@@ -45,7 +45,7 @@ export const noStringMatch: KlintRule = {
45
45
  })()
46
46
  : undefined;
47
47
  violations.push({
48
- file: relative(root, file).replaceAll("\\", "/"),
48
+ file: relativeSlashPath(root, file),
49
49
  line: s + 1,
50
50
  message: `Use RegExp.exec() instead of String.match() for non-global regexes — use new RegExp(${regexText}).exec(${strText}) instead.`,
51
51
  fix,
@@ -1,6 +1,6 @@
1
- import { relative } from "node:path";
2
1
  import ts from "typescript";
3
2
  import { nearestFunctionIsAsync, walkAst } from "../core/ast";
3
+ import { relativeSlashPath } from "../core/paths";
4
4
  import type { KlintRule } from "../core/types";
5
5
 
6
6
  export const noSyncInAsync: KlintRule = {
@@ -31,7 +31,7 @@ export const noSyncInAsync: KlintRule = {
31
31
  ) {
32
32
  const { line } = src.getLineAndCharacterOfPosition(node.getStart());
33
33
  violations.push({
34
- file: relative(root, file),
34
+ file: relativeSlashPath(root, file),
35
35
  line: line + 1,
36
36
  message: `${name}() blocks the event loop inside an async function — use the async equivalent from node:fs/promises.`,
37
37
  });
@@ -1,6 +1,6 @@
1
- import { relative } from "node:path";
2
1
  import ts from "typescript";
3
2
  import { isInsideTry, walkAst } from "../core/ast";
3
+ import { relativeSlashPath } from "../core/paths";
4
4
  import type { KlintRule } from "../core/types";
5
5
 
6
6
  export const noUnguardedJsonParse: KlintRule = {
@@ -23,7 +23,7 @@ export const noUnguardedJsonParse: KlintRule = {
23
23
  ) {
24
24
  const { line } = src.getLineAndCharacterOfPosition(node.getStart());
25
25
  violations.push({
26
- file: relative(root, file).replaceAll("\\", "/"),
26
+ file: relativeSlashPath(root, file),
27
27
  line: line + 1,
28
28
  message:
29
29
  "JSON.parse() called without a surrounding try/catch — a malformed payload will throw an unhandled exception.",
@@ -1,6 +1,6 @@
1
- import { relative } from "node:path";
2
1
  import ts from "typescript";
3
2
  import { walkAst } from "../core/ast";
3
+ import { relativeSlashPath } from "../core/paths";
4
4
  import { buildNodeReplacementFix } from "../core/rule-helpers";
5
5
  import type { KlintRule } from "../core/types";
6
6
 
@@ -35,7 +35,7 @@ export const preferAt: KlintRule = {
35
35
  const fix = buildNodeReplacementFix(src, node, fixedCall);
36
36
 
37
37
  violations.push({
38
- file: relative(root, file),
38
+ file: relativeSlashPath(root, file),
39
39
  line: fix.startLine,
40
40
  message: `Prefer ${baseText}.at(-${n}) over ${baseText}[${baseText}.length - ${n}] for cleaner negative indexing.`,
41
41
  fix,
@@ -1,6 +1,6 @@
1
- import { relative } from "node:path";
2
1
  import ts from "typescript";
3
2
  import { walkAst } from "../core/ast";
3
+ import { relativeSlashPath } from "../core/paths";
4
4
  import type { KlintRule } from "../core/types";
5
5
 
6
6
  export const preferNullishCoalescingAssign: KlintRule = {
@@ -33,7 +33,7 @@ export const preferNullishCoalescingAssign: KlintRule = {
33
33
  const indent = getIndent(src, node);
34
34
 
35
35
  violations.push({
36
- file: relative(root, file),
36
+ file: relativeSlashPath(root, file),
37
37
  line: s + 1,
38
38
  message: `Prefer \`${xText} ??= ${yText}\` over \`if (!${xText}) ${xText} = ${yText}\` — ??= only assigns when null or undefined.`,
39
39
  fix: {
@@ -1,6 +1,6 @@
1
- import { relative } from "node:path";
2
1
  import ts from "typescript";
3
2
  import { walkAst } from "../core/ast";
3
+ import { relativeSlashPath } from "../core/paths";
4
4
  import type { KlintRule } from "../core/types";
5
5
 
6
6
  export const preferStringRawRegexp: KlintRule = {
@@ -47,7 +47,7 @@ export const preferStringRawRegexp: KlintRule = {
47
47
  linesText.slice(0, argOffset) + fixedArg + linesText.slice(argEndOffset);
48
48
 
49
49
  violations.push({
50
- file: relative(root, file),
50
+ file: relativeSlashPath(root, file),
51
51
  line: line + 1,
52
52
  message:
53
53
  "Use String.raw`...` for RegExp template argument to avoid double backslashes (Sonar S7780).",
@@ -1,6 +1,6 @@
1
- import { relative } from "node:path";
2
1
  import ts from "typescript";
3
2
  import { walkAst } from "../core/ast";
3
+ import { relativeSlashPath } from "../core/paths";
4
4
  import { buildNodeReplacementFix } from "../core/rule-helpers";
5
5
  import type { KlintRule } from "../core/types";
6
6
 
@@ -27,7 +27,7 @@ export const preferStringRaw: KlintRule = {
27
27
  const fix = buildNodeReplacementFix(src, node, fixedNode);
28
28
 
29
29
  violations.push({
30
- file: relative(root, file),
30
+ file: relativeSlashPath(root, file),
31
31
  line: fix.startLine,
32
32
  message:
33
33
  "String literal with escaped backslashes — use String.raw`...` for clarity (Sonar S6535).",
@@ -1,6 +1,6 @@
1
- import { relative } from "node:path";
2
1
  import ts from "typescript";
3
2
  import { walkAst } from "../core/ast";
3
+ import { relativeSlashPath } from "../core/paths";
4
4
  import { buildNodeReplacementFix } from "../core/rule-helpers";
5
5
  import type { KlintRule } from "../core/types";
6
6
 
@@ -35,13 +35,13 @@ export const preferStringReplaceall: KlintRule = {
35
35
  ).expression.getText(src);
36
36
  const replacementText = node.arguments[1].getText(src);
37
37
  const patternLit = pattern.includes('"')
38
- ? `'${pattern.replaceAll("'", "\\'")}'`
38
+ ? `'${pattern.replaceAll("'", String.raw`\'`)}'`
39
39
  : `"${pattern}"`;
40
40
  const fixedCall = `${strText}.replaceAll(${patternLit}, ${replacementText})`;
41
41
  const fix = buildNodeReplacementFix(src, node, fixedCall);
42
42
 
43
43
  violations.push({
44
- file: relative(root, file),
44
+ file: relativeSlashPath(root, file),
45
45
  line: fix.startLine,
46
46
  message: `Prefer \`${strText}.replaceAll(${patternLit}, ...)\` over \`.replace(/${pattern}/g, ...)\` — replaceAll() with a string is clearer and avoids regex escaping pitfalls.`,
47
47
  fix,
@@ -28,22 +28,31 @@ export async function prepare(_, context) {
28
28
 
29
29
  const packageJson = readPackageJson(cwd);
30
30
  packageJson.version = version;
31
- packageJson.optionalDependencies ??= {};
32
- for (const packageName of NATIVE_PACKAGE_NAMES) {
33
- packageJson.optionalDependencies[packageName] = version;
34
- }
35
31
  writePackageJson(cwd, packageJson);
36
32
  }
37
33
 
38
- export async function publish(_, context) {
34
+ export async function publish(pluginConfig = {}, context) {
39
35
  const cwd = context.cwd ?? process.cwd();
36
+ const spawn = pluginConfig.spawnSync ?? spawnSync;
37
+ const version = context.nextRelease?.version;
40
38
 
41
39
  if (context.options?.dryRun) {
42
40
  context.logger?.log("Dry run: skipping npm publish");
43
41
  return;
44
42
  }
45
43
 
46
- const result = spawnSync("npm", ["publish", "--access", "public"], {
44
+ if (!version) {
45
+ throw new Error("semantic-release did not provide nextRelease.version");
46
+ }
47
+
48
+ const packageJson = readPackageJson(cwd);
49
+ packageJson.optionalDependencies ??= {};
50
+ for (const packageName of NATIVE_PACKAGE_NAMES) {
51
+ packageJson.optionalDependencies[packageName] = version;
52
+ }
53
+ writePackageJson(cwd, packageJson);
54
+
55
+ const result = spawn("npm", ["publish", "--access", "public"], {
47
56
  cwd,
48
57
  encoding: "utf-8",
49
58
  stdio: "inherit",