@konvert7/klint 0.37.1 → 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
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import ts from "typescript";
|
|
2
|
-
import { nearestFunctionIsAsync, walkAst } from "../core/ast";
|
|
3
|
-
import { relativeSlashPath } from "../core/paths";
|
|
4
|
-
import type { KlintRule } from "../core/types";
|
|
5
|
-
|
|
6
|
-
export const noSyncInAsync: KlintRule = {
|
|
7
|
-
meta: {
|
|
8
|
-
description:
|
|
9
|
-
"Flags synchronous I/O (`readFileSync`, `execSync`, etc.) inside async functions — blocks the event loop when an async equivalent is available.",
|
|
10
|
-
examples: [
|
|
11
|
-
"no-sync-in-async: error",
|
|
12
|
-
'no-sync-in-async:\n severity: warn\n include: ["src/server/**"]',
|
|
13
|
-
],
|
|
14
|
-
},
|
|
15
|
-
check({ files, root, fileContents }, violations) {
|
|
16
|
-
for (const file of files) {
|
|
17
|
-
const content = fileContents.get(file) ?? "";
|
|
18
|
-
walkAst(file, content, (node, src) => {
|
|
19
|
-
if (ts.isCallExpression(node)) {
|
|
20
|
-
const callee = node.expression;
|
|
21
|
-
let name: string | null = null;
|
|
22
|
-
if (ts.isIdentifier(callee)) {
|
|
23
|
-
name = callee.text;
|
|
24
|
-
} else if (ts.isPropertyAccessExpression(callee)) {
|
|
25
|
-
name = callee.name.text;
|
|
26
|
-
}
|
|
27
|
-
if (
|
|
28
|
-
name?.endsWith("Sync") &&
|
|
29
|
-
name !== "existsSync" &&
|
|
30
|
-
nearestFunctionIsAsync(node)
|
|
31
|
-
) {
|
|
32
|
-
const { line } = src.getLineAndCharacterOfPosition(node.getStart());
|
|
33
|
-
violations.push({
|
|
34
|
-
file: relativeSlashPath(root, file),
|
|
35
|
-
line: line + 1,
|
|
36
|
-
message: `${name}() blocks the event loop inside an async function — use the async equivalent from node:fs/promises.`,
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
},
|
|
43
|
-
};
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import ts from "typescript";
|
|
2
|
-
import { isInsideTry, walkAst } from "../core/ast";
|
|
3
|
-
import { relativeSlashPath } from "../core/paths";
|
|
4
|
-
import type { KlintRule } from "../core/types";
|
|
5
|
-
|
|
6
|
-
export const noUnguardedJsonParse: KlintRule = {
|
|
7
|
-
meta: {
|
|
8
|
-
description:
|
|
9
|
-
"Flags `JSON.parse()` calls that aren't wrapped in try/catch — malformed input crashes the process.",
|
|
10
|
-
examples: ["no-unguarded-json-parse: error"],
|
|
11
|
-
},
|
|
12
|
-
check({ files, root, fileContents }, violations) {
|
|
13
|
-
for (const file of files) {
|
|
14
|
-
const content = fileContents.get(file) ?? "";
|
|
15
|
-
walkAst(file, content, (node, src) => {
|
|
16
|
-
if (
|
|
17
|
-
ts.isCallExpression(node) &&
|
|
18
|
-
ts.isPropertyAccessExpression(node.expression) &&
|
|
19
|
-
node.expression.name.text === "parse" &&
|
|
20
|
-
ts.isIdentifier(node.expression.expression) &&
|
|
21
|
-
node.expression.expression.text === "JSON" &&
|
|
22
|
-
!isInsideTry(node)
|
|
23
|
-
) {
|
|
24
|
-
const { line } = src.getLineAndCharacterOfPosition(node.getStart());
|
|
25
|
-
violations.push({
|
|
26
|
-
file: relativeSlashPath(root, file),
|
|
27
|
-
line: line + 1,
|
|
28
|
-
message:
|
|
29
|
-
"JSON.parse() called without a surrounding try/catch — a malformed payload will throw an unhandled exception.",
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
},
|
|
35
|
-
};
|
package/rules/prefer-at.ts
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import ts from "typescript";
|
|
2
|
-
import { walkAst } from "../core/ast";
|
|
3
|
-
import { relativeSlashPath } from "../core/paths";
|
|
4
|
-
import { buildNodeReplacementFix } from "../core/rule-helpers";
|
|
5
|
-
import type { KlintRule } from "../core/types";
|
|
6
|
-
|
|
7
|
-
export const preferAt: KlintRule = {
|
|
8
|
-
meta: {
|
|
9
|
-
description:
|
|
10
|
-
"Flags negative-index access via `arr[arr.length - 1]` — `.at(-1)` reads cleaner for last-element access.",
|
|
11
|
-
examples: ["sonar/prefer-at: warn"],
|
|
12
|
-
},
|
|
13
|
-
check({ files, root, fileContents }, violations) {
|
|
14
|
-
for (const file of files) {
|
|
15
|
-
const content = fileContents.get(file) ?? "";
|
|
16
|
-
walkAst(file, content, (node, src) => {
|
|
17
|
-
// Match: base[base.length - n]
|
|
18
|
-
if (!ts.isElementAccessExpression(node)) return;
|
|
19
|
-
|
|
20
|
-
const arg = node.argumentExpression;
|
|
21
|
-
if (!ts.isBinaryExpression(arg)) return;
|
|
22
|
-
if (arg.operatorToken.kind !== ts.SyntaxKind.MinusToken) return;
|
|
23
|
-
if (!ts.isPropertyAccessExpression(arg.left)) return;
|
|
24
|
-
if (arg.left.name.text !== "length") return;
|
|
25
|
-
if (!ts.isNumericLiteral(arg.right)) return;
|
|
26
|
-
|
|
27
|
-
const n = Number(arg.right.text);
|
|
28
|
-
// n=0 changes semantics: arr[arr.length - 0] is undefined, arr.at(-0) is arr[0]
|
|
29
|
-
if (!Number.isInteger(n) || n <= 0) return;
|
|
30
|
-
|
|
31
|
-
const baseText = node.expression.getText(src);
|
|
32
|
-
if (baseText !== arg.left.expression.getText(src)) return;
|
|
33
|
-
|
|
34
|
-
const fixedCall = `${baseText}.at(-${n})`;
|
|
35
|
-
const fix = buildNodeReplacementFix(src, node, fixedCall);
|
|
36
|
-
|
|
37
|
-
violations.push({
|
|
38
|
-
file: relativeSlashPath(root, file),
|
|
39
|
-
line: fix.startLine,
|
|
40
|
-
message: `Prefer ${baseText}.at(-${n}) over ${baseText}[${baseText}.length - ${n}] for cleaner negative indexing.`,
|
|
41
|
-
fix,
|
|
42
|
-
});
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
},
|
|
46
|
-
};
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
import ts from "typescript";
|
|
2
|
-
import { walkAst } from "../core/ast";
|
|
3
|
-
import { relativeSlashPath } from "../core/paths";
|
|
4
|
-
import type { KlintRule } from "../core/types";
|
|
5
|
-
|
|
6
|
-
export const preferNullishCoalescingAssign: KlintRule = {
|
|
7
|
-
meta: {
|
|
8
|
-
description:
|
|
9
|
-
"Flags explicit nullish assignment guards — `x ??= y` is the dedicated nullish assignment operator.",
|
|
10
|
-
examples: ["sonar/prefer-nullish-coalescing-assign: warn"],
|
|
11
|
-
},
|
|
12
|
-
check({ files, root, fileContents }, violations) {
|
|
13
|
-
for (const file of files) {
|
|
14
|
-
const content = fileContents.get(file) ?? "";
|
|
15
|
-
walkAst(file, content, (node, src) => {
|
|
16
|
-
if (!ts.isIfStatement(node) || node.elseStatement !== undefined) return;
|
|
17
|
-
|
|
18
|
-
const assignExpr = extractAssignment(node.thenStatement);
|
|
19
|
-
if (!assignExpr) return;
|
|
20
|
-
|
|
21
|
-
const xText = nullishGuardTarget(node.expression, src);
|
|
22
|
-
if (!xText) return;
|
|
23
|
-
if (assignExpr.left.getText(src) !== xText) return;
|
|
24
|
-
|
|
25
|
-
const yText = assignExpr.right.getText(src);
|
|
26
|
-
const { line: s } = src.getLineAndCharacterOfPosition(node.getStart());
|
|
27
|
-
const { line: e } = src.getLineAndCharacterOfPosition(node.getEnd());
|
|
28
|
-
const indent = getIndent(src, node);
|
|
29
|
-
|
|
30
|
-
violations.push({
|
|
31
|
-
file: relativeSlashPath(root, file),
|
|
32
|
-
line: s + 1,
|
|
33
|
-
message: `Prefer \`${xText} ??= ${yText}\` over explicit nullish guard assignment — ??= only assigns when null or undefined.`,
|
|
34
|
-
fix: {
|
|
35
|
-
startLine: s + 1,
|
|
36
|
-
endLine: e + 1,
|
|
37
|
-
replacement: `${indent}${xText} ??= ${yText};`,
|
|
38
|
-
},
|
|
39
|
-
});
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
},
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
function extractAssignment(stmt: ts.Statement): ts.BinaryExpression | undefined {
|
|
46
|
-
if (ts.isExpressionStatement(stmt)) {
|
|
47
|
-
return isAssign(stmt.expression) ? stmt.expression : undefined;
|
|
48
|
-
}
|
|
49
|
-
if (ts.isBlock(stmt) && stmt.statements.length === 1) {
|
|
50
|
-
const inner = stmt.statements[0];
|
|
51
|
-
if (ts.isExpressionStatement(inner) && isAssign(inner.expression)) {
|
|
52
|
-
return inner.expression;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
return undefined;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function isAssign(node: ts.Expression): node is ts.BinaryExpression {
|
|
59
|
-
return (
|
|
60
|
-
ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken
|
|
61
|
-
);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function nullishGuardTarget(
|
|
65
|
-
expression: ts.Expression,
|
|
66
|
-
src: ts.SourceFile
|
|
67
|
-
): string | undefined {
|
|
68
|
-
if (!ts.isBinaryExpression(expression)) return undefined;
|
|
69
|
-
|
|
70
|
-
const direct = equalityNullishTarget(expression, src);
|
|
71
|
-
if (direct) return direct.target;
|
|
72
|
-
|
|
73
|
-
if (expression.operatorToken.kind !== ts.SyntaxKind.BarBarToken) return undefined;
|
|
74
|
-
|
|
75
|
-
const left = equalityNullishTarget(expression.left, src);
|
|
76
|
-
const right = equalityNullishTarget(expression.right, src);
|
|
77
|
-
if (!left || !right || left.target !== right.target) return undefined;
|
|
78
|
-
|
|
79
|
-
const values = new Set([left.value, right.value]);
|
|
80
|
-
if (values.has("null") && values.has("undefined")) return left.target;
|
|
81
|
-
return undefined;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function equalityNullishTarget(
|
|
85
|
-
expression: ts.Expression,
|
|
86
|
-
src: ts.SourceFile
|
|
87
|
-
): { target: string; value: "null" | "undefined" } | undefined {
|
|
88
|
-
if (!ts.isBinaryExpression(expression)) return undefined;
|
|
89
|
-
|
|
90
|
-
const operator = expression.operatorToken.kind;
|
|
91
|
-
if (
|
|
92
|
-
operator !== ts.SyntaxKind.EqualsEqualsToken &&
|
|
93
|
-
operator !== ts.SyntaxKind.EqualsEqualsEqualsToken
|
|
94
|
-
)
|
|
95
|
-
return undefined;
|
|
96
|
-
|
|
97
|
-
const left = expression.left.getText(src);
|
|
98
|
-
const right = expression.right.getText(src);
|
|
99
|
-
|
|
100
|
-
if (right === "null") return { target: left, value: "null" };
|
|
101
|
-
if (right === "undefined") return { target: left, value: "undefined" };
|
|
102
|
-
if (left === "null") return { target: right, value: "null" };
|
|
103
|
-
if (left === "undefined") return { target: right, value: "undefined" };
|
|
104
|
-
return undefined;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function getIndent(src: ts.SourceFile, node: ts.Node): string {
|
|
108
|
-
const { line } = src.getLineAndCharacterOfPosition(node.getStart());
|
|
109
|
-
const lineStart = src.getPositionOfLineAndCharacter(line, 0);
|
|
110
|
-
return new RegExp(/^(\s*)/).exec(src.text.slice(lineStart, node.getStart()))?.[1] ?? "";
|
|
111
|
-
}
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
import ts from "typescript";
|
|
2
|
-
import { walkAst } from "../core/ast";
|
|
3
|
-
import { relativeSlashPath } from "../core/paths";
|
|
4
|
-
import type { KlintRule } from "../core/types";
|
|
5
|
-
|
|
6
|
-
export const preferStringRawRegexp: KlintRule = {
|
|
7
|
-
meta: {
|
|
8
|
-
description:
|
|
9
|
-
"Flags regex literals built from escape-heavy string concatenation — `String.raw` drops the double backslashes.",
|
|
10
|
-
examples: ["sonar/prefer-string-raw-regexp: warn"],
|
|
11
|
-
},
|
|
12
|
-
check({ files, root, fileContents }, violations) {
|
|
13
|
-
for (const file of files) {
|
|
14
|
-
const content = fileContents.get(file) ?? "";
|
|
15
|
-
walkAst(file, content, (node, src) => {
|
|
16
|
-
if (
|
|
17
|
-
!ts.isNewExpression(node) ||
|
|
18
|
-
!ts.isIdentifier(node.expression) ||
|
|
19
|
-
node.expression.text !== "RegExp" ||
|
|
20
|
-
!node.arguments ||
|
|
21
|
-
node.arguments.length === 0
|
|
22
|
-
)
|
|
23
|
-
return;
|
|
24
|
-
|
|
25
|
-
const arg = node.arguments[0];
|
|
26
|
-
if (!ts.isNoSubstitutionTemplateLiteral(arg) && !ts.isTemplateExpression(arg))
|
|
27
|
-
return;
|
|
28
|
-
|
|
29
|
-
if (!hasDoubleBackslash(arg as ts.TemplateLiteral, src)) return;
|
|
30
|
-
|
|
31
|
-
const { line } = src.getLineAndCharacterOfPosition(node.getStart());
|
|
32
|
-
const { line: argStartLine } = src.getLineAndCharacterOfPosition(arg.getStart());
|
|
33
|
-
const { line: argEndLine } = src.getLineAndCharacterOfPosition(arg.getEnd());
|
|
34
|
-
|
|
35
|
-
const lineStarts = src.getLineStarts();
|
|
36
|
-
const lineStart = lineStarts[argStartLine];
|
|
37
|
-
const lineEnd =
|
|
38
|
-
argEndLine + 1 < lineStarts.length
|
|
39
|
-
? lineStarts[argEndLine + 1] - 1
|
|
40
|
-
: src.text.length;
|
|
41
|
-
const linesText = src.text.slice(lineStart, lineEnd);
|
|
42
|
-
|
|
43
|
-
const argOffset = arg.getStart() - lineStart;
|
|
44
|
-
const argEndOffset = arg.getEnd() - lineStart;
|
|
45
|
-
const fixedArg = toStringRaw(arg as ts.TemplateLiteral, src);
|
|
46
|
-
const fixedLines =
|
|
47
|
-
linesText.slice(0, argOffset) + fixedArg + linesText.slice(argEndOffset);
|
|
48
|
-
|
|
49
|
-
violations.push({
|
|
50
|
-
file: relativeSlashPath(root, file),
|
|
51
|
-
line: line + 1,
|
|
52
|
-
message:
|
|
53
|
-
"Use String.raw`...` for RegExp template argument to avoid double backslashes (Sonar S7780).",
|
|
54
|
-
fix: {
|
|
55
|
-
startLine: argStartLine + 1,
|
|
56
|
-
endLine: argEndLine + 1,
|
|
57
|
-
replacement: fixedLines,
|
|
58
|
-
},
|
|
59
|
-
});
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
},
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
function hasDoubleBackslash(node: ts.TemplateLiteral, src: ts.SourceFile): boolean {
|
|
66
|
-
if (ts.isNoSubstitutionTemplateLiteral(node)) {
|
|
67
|
-
return node.getText(src).includes("\\\\");
|
|
68
|
-
}
|
|
69
|
-
if (node.head.getText(src).includes("\\\\")) return true;
|
|
70
|
-
for (const span of node.templateSpans) {
|
|
71
|
-
if (span.literal.getText(src).includes("\\\\")) return true;
|
|
72
|
-
}
|
|
73
|
-
return false;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function toStringRaw(node: ts.TemplateLiteral, src: ts.SourceFile): string {
|
|
77
|
-
if (ts.isNoSubstitutionTemplateLiteral(node)) {
|
|
78
|
-
const raw = node.getText(src);
|
|
79
|
-
const inner = raw.slice(1, -1);
|
|
80
|
-
return `String.raw\`${inner.replaceAll("\\\\", "\\")}\``;
|
|
81
|
-
}
|
|
82
|
-
let result = "String.raw`";
|
|
83
|
-
const headSrc = node.head.getText(src);
|
|
84
|
-
result += `${headSrc.slice(1, -2).replaceAll("\\\\", "\\")}\${`;
|
|
85
|
-
for (const span of node.templateSpans) {
|
|
86
|
-
result += span.expression.getText(src);
|
|
87
|
-
const litSrc = span.literal.getText(src);
|
|
88
|
-
const isMiddle = ts.isTemplateMiddle(span.literal);
|
|
89
|
-
const litContent = isMiddle ? litSrc.slice(1, -2) : litSrc.slice(1, -1);
|
|
90
|
-
result += `}${litContent.replaceAll("\\\\", "\\")}${isMiddle ? "${" : "`"}`;
|
|
91
|
-
}
|
|
92
|
-
return result;
|
|
93
|
-
}
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import ts from "typescript";
|
|
2
|
-
import { walkAst } from "../core/ast";
|
|
3
|
-
import { relativeSlashPath } from "../core/paths";
|
|
4
|
-
import { buildNodeReplacementFix } from "../core/rule-helpers";
|
|
5
|
-
import type { KlintRule } from "../core/types";
|
|
6
|
-
|
|
7
|
-
export const preferStringRaw: KlintRule = {
|
|
8
|
-
meta: {
|
|
9
|
-
description:
|
|
10
|
-
"Flags string literals with multiple backslash escapes — `String.raw` is more readable (Windows paths, regex source, etc.).",
|
|
11
|
-
examples: ["sonar/prefer-string-raw: warn"],
|
|
12
|
-
},
|
|
13
|
-
check({ files, root, fileContents }, violations) {
|
|
14
|
-
for (const file of files) {
|
|
15
|
-
const content = fileContents.get(file) ?? "";
|
|
16
|
-
walkAst(file, content, (node, src) => {
|
|
17
|
-
if (!ts.isStringLiteral(node)) return;
|
|
18
|
-
|
|
19
|
-
const sourceText = node.getText(src);
|
|
20
|
-
if (!sourceText.includes("\\\\")) return;
|
|
21
|
-
|
|
22
|
-
const value = node.text;
|
|
23
|
-
// Can't embed backtick, ${ (would start interpolation), or trailing \ (would escape closing backtick)
|
|
24
|
-
if (value.includes("`") || value.includes("${") || value.endsWith("\\")) return;
|
|
25
|
-
|
|
26
|
-
const fixedNode = `String.raw\`${value}\``;
|
|
27
|
-
const fix = buildNodeReplacementFix(src, node, fixedNode);
|
|
28
|
-
|
|
29
|
-
violations.push({
|
|
30
|
-
file: relativeSlashPath(root, file),
|
|
31
|
-
line: fix.startLine,
|
|
32
|
-
message:
|
|
33
|
-
"String literal with escaped backslashes — use String.raw`...` for clarity (Sonar S6535).",
|
|
34
|
-
fix,
|
|
35
|
-
});
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
},
|
|
39
|
-
};
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import ts from "typescript";
|
|
2
|
-
import { walkAst } from "../core/ast";
|
|
3
|
-
import { relativeSlashPath } from "../core/paths";
|
|
4
|
-
import { buildNodeReplacementFix } from "../core/rule-helpers";
|
|
5
|
-
import type { KlintRule } from "../core/types";
|
|
6
|
-
|
|
7
|
-
export const preferStringReplaceall: KlintRule = {
|
|
8
|
-
meta: {
|
|
9
|
-
description:
|
|
10
|
-
"Flags `.replace(/x/g, y)` on strings — `.replaceAll('x', y)` is clearer and faster for all-occurrence replacements.",
|
|
11
|
-
examples: ["sonar/prefer-string-replaceall: error"],
|
|
12
|
-
},
|
|
13
|
-
check({ files, root, fileContents }, violations) {
|
|
14
|
-
for (const file of files) {
|
|
15
|
-
const content = fileContents.get(file) ?? "";
|
|
16
|
-
walkAst(file, content, (node, src) => {
|
|
17
|
-
if (
|
|
18
|
-
!ts.isCallExpression(node) ||
|
|
19
|
-
!ts.isPropertyAccessExpression(node.expression) ||
|
|
20
|
-
node.expression.name.text !== "replace" ||
|
|
21
|
-
node.arguments.length !== 2 ||
|
|
22
|
-
!ts.isRegularExpressionLiteral(node.arguments[0])
|
|
23
|
-
)
|
|
24
|
-
return;
|
|
25
|
-
|
|
26
|
-
const regexSrc = node.arguments[0].getText(src);
|
|
27
|
-
const flags = extractFlags(regexSrc);
|
|
28
|
-
const pattern = extractPattern(regexSrc);
|
|
29
|
-
|
|
30
|
-
if (flags !== "g") return;
|
|
31
|
-
if (!isPlainLiteral(pattern)) return;
|
|
32
|
-
|
|
33
|
-
const strText = (
|
|
34
|
-
node.expression as ts.PropertyAccessExpression
|
|
35
|
-
).expression.getText(src);
|
|
36
|
-
const replacementText = node.arguments[1].getText(src);
|
|
37
|
-
const patternLit = pattern.includes('"')
|
|
38
|
-
? `'${pattern.replaceAll("'", String.raw`\'`)}'`
|
|
39
|
-
: `"${pattern}"`;
|
|
40
|
-
const fixedCall = `${strText}.replaceAll(${patternLit}, ${replacementText})`;
|
|
41
|
-
const fix = buildNodeReplacementFix(src, node, fixedCall);
|
|
42
|
-
|
|
43
|
-
violations.push({
|
|
44
|
-
file: relativeSlashPath(root, file),
|
|
45
|
-
line: fix.startLine,
|
|
46
|
-
message: `Prefer \`${strText}.replaceAll(${patternLit}, ...)\` over \`.replace(/${pattern}/g, ...)\` — replaceAll() with a string is clearer and avoids regex escaping pitfalls.`,
|
|
47
|
-
fix,
|
|
48
|
-
});
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
},
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
function extractFlags(literal: string): string {
|
|
55
|
-
const last = literal.lastIndexOf("/");
|
|
56
|
-
return last > 0 ? literal.slice(last + 1) : "";
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function extractPattern(literal: string): string {
|
|
60
|
-
return literal.slice(1, literal.lastIndexOf("/"));
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function isPlainLiteral(pattern: string): boolean {
|
|
64
|
-
// Strip escaped-backslash pairs (\\) before checking — \\ is a plain literal, not a metachar
|
|
65
|
-
const stripped = pattern.replaceAll("\\\\", "");
|
|
66
|
-
return !/[.*+?[\]{}()|^$\\]/.test(stripped);
|
|
67
|
-
}
|