@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.
- 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/plugins/index.ts
DELETED
package/plugins/sonar.ts
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import type { KlintPlugin } from "../core/types";
|
|
2
|
-
import { noSingleCharClass } from "../rules/no-single-char-class";
|
|
3
|
-
import { preferAt } from "../rules/prefer-at";
|
|
4
|
-
import { preferNullishCoalescingAssign } from "../rules/prefer-nullish-coalescing-assign";
|
|
5
|
-
import { preferStringRaw } from "../rules/prefer-string-raw";
|
|
6
|
-
import { preferStringRawRegexp } from "../rules/prefer-string-raw-regexp";
|
|
7
|
-
import { preferStringReplaceall } from "../rules/prefer-string-replaceall";
|
|
8
|
-
|
|
9
|
-
export const sonarPlugin: KlintPlugin = {
|
|
10
|
-
name: "sonar",
|
|
11
|
-
rules: {
|
|
12
|
-
"sonar/prefer-string-replaceall": "error",
|
|
13
|
-
"sonar/prefer-string-raw-regexp": "error",
|
|
14
|
-
"sonar/prefer-string-raw": "error",
|
|
15
|
-
"sonar/prefer-nullish-coalescing-assign": "error",
|
|
16
|
-
"sonar/no-single-char-class": "error",
|
|
17
|
-
"sonar/prefer-at": "error",
|
|
18
|
-
},
|
|
19
|
-
implementations: {
|
|
20
|
-
"sonar/prefer-string-replaceall": preferStringReplaceall,
|
|
21
|
-
"sonar/prefer-string-raw-regexp": preferStringRawRegexp,
|
|
22
|
-
"sonar/prefer-string-raw": preferStringRaw,
|
|
23
|
-
"sonar/prefer-nullish-coalescing-assign": preferNullishCoalescingAssign,
|
|
24
|
-
"sonar/no-single-char-class": noSingleCharClass,
|
|
25
|
-
"sonar/prefer-at": preferAt,
|
|
26
|
-
},
|
|
27
|
-
};
|
package/rules/index.ts
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import type { KlintRule } from "../core/types";
|
|
2
|
-
import { noAsyncPredicate } from "./no-async-predicate";
|
|
3
|
-
import { noConsecutiveArrayPush } from "./no-consecutive-array-push";
|
|
4
|
-
import { noDateEquality } from "./no-date-equality";
|
|
5
|
-
import { noFloatingPromise } from "./no-floating-promise";
|
|
6
|
-
import { noMisusedPromises } from "./no-misused-promises";
|
|
7
|
-
import { noNestedTemplateLiterals } from "./no-nested-template-literals";
|
|
8
|
-
import { noObjectInTemplate } from "./no-object-in-template";
|
|
9
|
-
import { noOptionalChainOnNonNullable } from "./no-optional-chain-on-non-nullable";
|
|
10
|
-
import { noStringMatch } from "./no-string-match";
|
|
11
|
-
import { noSyncInAsync } from "./no-sync-in-async";
|
|
12
|
-
import { noUnguardedJsonParse } from "./no-unguarded-json-parse";
|
|
13
|
-
|
|
14
|
-
export const BUILT_IN_RULES: Record<string, KlintRule> = {
|
|
15
|
-
"no-unguarded-json-parse": noUnguardedJsonParse,
|
|
16
|
-
"no-sync-in-async": noSyncInAsync,
|
|
17
|
-
"no-floating-promise": noFloatingPromise,
|
|
18
|
-
"no-misused-promises": noMisusedPromises,
|
|
19
|
-
"no-async-predicate": noAsyncPredicate,
|
|
20
|
-
"no-date-equality": noDateEquality,
|
|
21
|
-
"no-optional-chain-on-non-nullable": noOptionalChainOnNonNullable,
|
|
22
|
-
"no-object-in-template": noObjectInTemplate,
|
|
23
|
-
"no-nested-template-literals": noNestedTemplateLiterals,
|
|
24
|
-
"no-consecutive-array-push": noConsecutiveArrayPush,
|
|
25
|
-
"no-string-match": noStringMatch,
|
|
26
|
-
};
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import ts from "typescript";
|
|
2
|
-
import { defineAstRule } from "../core/rule-helpers";
|
|
3
|
-
|
|
4
|
-
const PREDICATE_METHODS = new Set(["filter", "some", "every", "find", "findIndex"]);
|
|
5
|
-
|
|
6
|
-
export const noAsyncPredicate = defineAstRule({
|
|
7
|
-
meta: {
|
|
8
|
-
description:
|
|
9
|
-
"Flags `async` functions passed to `.filter`/`.some`/`.every`/`.find`/`.findIndex`. Array methods coerce the returned promise to truthy, so every element appears to match.",
|
|
10
|
-
examples: ["no-async-predicate: error"],
|
|
11
|
-
},
|
|
12
|
-
visit(node, ctx) {
|
|
13
|
-
if (
|
|
14
|
-
!ts.isCallExpression(node) ||
|
|
15
|
-
!ts.isPropertyAccessExpression(node.expression) ||
|
|
16
|
-
!PREDICATE_METHODS.has(node.expression.name.text) ||
|
|
17
|
-
node.arguments.length === 0
|
|
18
|
-
)
|
|
19
|
-
return;
|
|
20
|
-
|
|
21
|
-
const receiverType = ctx.checker.getTypeAtLocation(node.expression.expression);
|
|
22
|
-
if (isArrayLike(receiverType) && isAsyncFunction(node.arguments[0])) {
|
|
23
|
-
const method = node.expression.name.text;
|
|
24
|
-
ctx.report(
|
|
25
|
-
node.arguments[0],
|
|
26
|
-
`Async callback passed to .${method}() — the returned Promise is always truthy, so the predicate never filters correctly. The array method cannot await it.`
|
|
27
|
-
);
|
|
28
|
-
}
|
|
29
|
-
},
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
function isArrayLike(type: ts.Type): boolean {
|
|
33
|
-
if (type.isUnion()) return type.types.some((t) => isArrayLike(t));
|
|
34
|
-
const sym = type.getSymbol();
|
|
35
|
-
if (sym?.name === "Array" || sym?.name === "ReadonlyArray") return true;
|
|
36
|
-
const ref = type as ts.TypeReference;
|
|
37
|
-
if (ref.target) {
|
|
38
|
-
const targetSym = ref.target.getSymbol();
|
|
39
|
-
if (targetSym?.name === "Array" || targetSym?.name === "ReadonlyArray") return true;
|
|
40
|
-
}
|
|
41
|
-
return false;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function isAsyncFunction(
|
|
45
|
-
node: ts.Node
|
|
46
|
-
): node is ts.ArrowFunction | ts.FunctionExpression {
|
|
47
|
-
return (
|
|
48
|
-
(ts.isArrowFunction(node) || ts.isFunctionExpression(node)) &&
|
|
49
|
-
(node.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) ?? false)
|
|
50
|
-
);
|
|
51
|
-
}
|
|
@@ -1,61 +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 noConsecutiveArrayPush: KlintRule = {
|
|
7
|
-
meta: {
|
|
8
|
-
description:
|
|
9
|
-
"Flags consecutive `arr.push(x); arr.push(y);` calls — combine into `arr.push(x, y)` or `arr.push(...items)`.",
|
|
10
|
-
examples: ["no-consecutive-array-push: 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
|
-
const statements =
|
|
17
|
-
ts.isBlock(node) || ts.isSourceFile(node) ? node.statements : null;
|
|
18
|
-
if (!statements) return;
|
|
19
|
-
|
|
20
|
-
let runStart = -1;
|
|
21
|
-
let runReceiver = "";
|
|
22
|
-
|
|
23
|
-
const flush = (upTo: number) => {
|
|
24
|
-
if (runStart !== -1 && upTo - runStart >= 2) {
|
|
25
|
-
const { line } = src.getLineAndCharacterOfPosition(
|
|
26
|
-
statements[runStart].getStart()
|
|
27
|
-
);
|
|
28
|
-
violations.push({
|
|
29
|
-
file: relativeSlashPath(root, file),
|
|
30
|
-
line: line + 1,
|
|
31
|
-
message: `${upTo - runStart} consecutive .push() calls on \`${runReceiver}\` — combine into a single .push(a, b, …) call.`,
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
runStart = -1;
|
|
35
|
-
runReceiver = "";
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
for (let i = 0; i < statements.length; i++) {
|
|
39
|
-
const receiver = getPushReceiver(statements[i], src);
|
|
40
|
-
if (receiver && receiver === runReceiver) {
|
|
41
|
-
// continue the run
|
|
42
|
-
} else {
|
|
43
|
-
flush(i);
|
|
44
|
-
runStart = receiver ? i : -1;
|
|
45
|
-
runReceiver = receiver ?? "";
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
flush(statements.length);
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
},
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
function getPushReceiver(node: ts.Statement, src: ts.SourceFile): string | null {
|
|
55
|
-
if (!ts.isExpressionStatement(node)) return null;
|
|
56
|
-
const expr = node.expression;
|
|
57
|
-
if (!ts.isCallExpression(expr)) return null;
|
|
58
|
-
if (!ts.isPropertyAccessExpression(expr.expression)) return null;
|
|
59
|
-
if (expr.expression.name.text !== "push") return null;
|
|
60
|
-
return expr.expression.expression.getText(src);
|
|
61
|
-
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import ts from "typescript";
|
|
2
|
-
import { defineAstRule } from "../core/rule-helpers";
|
|
3
|
-
|
|
4
|
-
export const noDateEquality = defineAstRule({
|
|
5
|
-
meta: {
|
|
6
|
-
description:
|
|
7
|
-
"Flags `Date` comparisons with `==`/`===`/`!=`/`!==` — these compare object identity, not the time value. Use `.getTime()` instead.",
|
|
8
|
-
examples: ["no-date-equality: error"],
|
|
9
|
-
},
|
|
10
|
-
visit(node, ctx) {
|
|
11
|
-
if (!ts.isBinaryExpression(node)) return;
|
|
12
|
-
const { kind } = node.operatorToken;
|
|
13
|
-
if (
|
|
14
|
-
kind !== ts.SyntaxKind.EqualsEqualsEqualsToken &&
|
|
15
|
-
kind !== ts.SyntaxKind.ExclamationEqualsEqualsToken
|
|
16
|
-
)
|
|
17
|
-
return;
|
|
18
|
-
|
|
19
|
-
const leftType = ctx.checker.getTypeAtLocation(node.left);
|
|
20
|
-
const rightType = ctx.checker.getTypeAtLocation(node.right);
|
|
21
|
-
if (isDateType(leftType) && isDateType(rightType)) {
|
|
22
|
-
const op = node.operatorToken.getText(ctx.sourceFile);
|
|
23
|
-
ctx.report(
|
|
24
|
-
node,
|
|
25
|
-
`Date values compared with ${op} — this compares object references, not time values. Use .getTime() or .valueOf() instead.`
|
|
26
|
-
);
|
|
27
|
-
}
|
|
28
|
-
},
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
function isDateType(type: ts.Type): boolean {
|
|
32
|
-
if (type.getSymbol()?.name === "Date") return true;
|
|
33
|
-
if (type.isUnion()) return type.types.some(isDateType);
|
|
34
|
-
return false;
|
|
35
|
-
}
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import ts from "typescript";
|
|
2
|
-
import { defineAstRule } from "../core/rule-helpers";
|
|
3
|
-
|
|
4
|
-
export const noFloatingPromise = defineAstRule({
|
|
5
|
-
meta: {
|
|
6
|
-
description:
|
|
7
|
-
"Flags Promise-returning expressions that aren't awaited or `.catch()`-handled — silent failures and unpredictable execution order.",
|
|
8
|
-
examples: ["no-floating-promise: error"],
|
|
9
|
-
},
|
|
10
|
-
visit(node, ctx) {
|
|
11
|
-
if (
|
|
12
|
-
ts.isExpressionStatement(node) &&
|
|
13
|
-
ts.isCallExpression(node.expression) &&
|
|
14
|
-
!isHandled(node.expression)
|
|
15
|
-
) {
|
|
16
|
-
const type = ctx.checker.getTypeAtLocation(node.expression);
|
|
17
|
-
if (isPromiseLike(type)) {
|
|
18
|
-
ctx.report(
|
|
19
|
-
node,
|
|
20
|
-
"Promise-returning call is not awaited — errors will be silently discarded and execution order is unpredictable. Use await, void, or .catch()."
|
|
21
|
-
);
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
},
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
function isHandled(node: ts.CallExpression): boolean {
|
|
28
|
-
if (!ts.isPropertyAccessExpression(node.expression)) return false;
|
|
29
|
-
const name = node.expression.name.text;
|
|
30
|
-
return name === "catch" || name === "finally";
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function isPromiseLike(type: ts.Type): boolean {
|
|
34
|
-
if (type.getSymbol()?.name === "Promise") return true;
|
|
35
|
-
if (type.isUnion()) return type.types.some(isPromiseLike);
|
|
36
|
-
return false;
|
|
37
|
-
}
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import ts from "typescript";
|
|
2
|
-
import { defineAstRule } from "../core/rule-helpers";
|
|
3
|
-
|
|
4
|
-
export const noMisusedPromises = defineAstRule({
|
|
5
|
-
meta: {
|
|
6
|
-
description:
|
|
7
|
-
"Flags Promises passed where the type expects a non-thenable (e.g. an `if` condition, a `void` callback). The Promise's truthiness — not its resolved value — drives the branch.",
|
|
8
|
-
examples: ["no-misused-promises: error"],
|
|
9
|
-
},
|
|
10
|
-
visit(node, ctx) {
|
|
11
|
-
if (!ts.isCallExpression(node)) return;
|
|
12
|
-
const sig = ctx.checker.getResolvedSignature(node);
|
|
13
|
-
if (!sig) return;
|
|
14
|
-
const params = sig.getParameters();
|
|
15
|
-
for (let i = 0; i < node.arguments.length; i++) {
|
|
16
|
-
const arg = node.arguments[i];
|
|
17
|
-
if (!isAsyncFunction(arg)) continue;
|
|
18
|
-
|
|
19
|
-
const param = params[Math.min(i, params.length - 1)];
|
|
20
|
-
if (!param) continue;
|
|
21
|
-
|
|
22
|
-
const paramType = ctx.checker.getTypeOfSymbolAtLocation(param, node);
|
|
23
|
-
if (expectsSyncCallback(paramType, ctx.checker)) {
|
|
24
|
-
ctx.report(
|
|
25
|
-
arg,
|
|
26
|
-
"Async function passed where a sync callback is expected — the caller cannot await it and errors will be silently lost."
|
|
27
|
-
);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
},
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
function isAsyncFunction(
|
|
34
|
-
node: ts.Node
|
|
35
|
-
): node is ts.ArrowFunction | ts.FunctionExpression {
|
|
36
|
-
return (
|
|
37
|
-
(ts.isArrowFunction(node) || ts.isFunctionExpression(node)) &&
|
|
38
|
-
(node.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) ?? false)
|
|
39
|
-
);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function expectsSyncCallback(type: ts.Type, checker: ts.TypeChecker): boolean {
|
|
43
|
-
if (type.isUnion()) {
|
|
44
|
-
const callable = type.types.filter(
|
|
45
|
-
(t) => !(t.flags & ts.TypeFlags.Undefined) && !(t.flags & ts.TypeFlags.Null)
|
|
46
|
-
);
|
|
47
|
-
return callable.length > 0 && callable.every((t) => expectsSyncCallback(t, checker));
|
|
48
|
-
}
|
|
49
|
-
const sigs = type.getCallSignatures();
|
|
50
|
-
if (sigs.length === 0) return false;
|
|
51
|
-
return sigs.every((sig) => !returnsPromise(checker.getReturnTypeOfSignature(sig)));
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function returnsPromise(type: ts.Type): boolean {
|
|
55
|
-
const name = type.getSymbol()?.name;
|
|
56
|
-
if (name === "Promise" || name === "PromiseLike") return true;
|
|
57
|
-
if (type.isUnion()) return type.types.some(returnsPromise);
|
|
58
|
-
return false;
|
|
59
|
-
}
|
|
@@ -1,47 +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 noNestedTemplateLiterals: KlintRule = {
|
|
7
|
-
meta: {
|
|
8
|
-
description:
|
|
9
|
-
"Flags template literals nested inside template literals — almost always unreadable; extract to a variable.",
|
|
10
|
-
examples: ["no-nested-template-literals: 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.isTemplateExpression(node)) return;
|
|
17
|
-
for (const span of node.templateSpans) {
|
|
18
|
-
findNestedTemplate(span.expression, src, file, root, violations);
|
|
19
|
-
}
|
|
20
|
-
});
|
|
21
|
-
}
|
|
22
|
-
},
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
function findNestedTemplate(
|
|
26
|
-
node: ts.Node,
|
|
27
|
-
src: ts.SourceFile,
|
|
28
|
-
file: string,
|
|
29
|
-
root: string,
|
|
30
|
-
violations: ReturnType<typeof Array.prototype.slice>
|
|
31
|
-
): void {
|
|
32
|
-
if (ts.isTemplateExpression(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
|
|
33
|
-
const { line } = src.getLineAndCharacterOfPosition(node.getStart());
|
|
34
|
-
violations.push({
|
|
35
|
-
file: relativeSlashPath(root, file),
|
|
36
|
-
line: line + 1,
|
|
37
|
-
message:
|
|
38
|
-
"Nested template literal — extract the inner template to a variable to improve readability.",
|
|
39
|
-
});
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
// Don't descend into tagged templates — they are a single semantic unit
|
|
43
|
-
if (ts.isTaggedTemplateExpression(node)) return;
|
|
44
|
-
ts.forEachChild(node, (child) =>
|
|
45
|
-
findNestedTemplate(child, src, file, root, violations)
|
|
46
|
-
);
|
|
47
|
-
}
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import ts from "typescript";
|
|
2
|
-
import { defineAstRule } from "../core/rule-helpers";
|
|
3
|
-
|
|
4
|
-
// Builtins with a meaningful toString() that isn't [object Object]
|
|
5
|
-
const SAFE_SYMBOL_NAMES = new Set([
|
|
6
|
-
"Date",
|
|
7
|
-
"RegExp",
|
|
8
|
-
"Error",
|
|
9
|
-
"TypeError",
|
|
10
|
-
"RangeError",
|
|
11
|
-
"SyntaxError",
|
|
12
|
-
"ReferenceError",
|
|
13
|
-
"Array",
|
|
14
|
-
"Map",
|
|
15
|
-
"Set",
|
|
16
|
-
"URL",
|
|
17
|
-
"URLSearchParams",
|
|
18
|
-
"Symbol",
|
|
19
|
-
]);
|
|
20
|
-
|
|
21
|
-
export const noObjectInTemplate = defineAstRule({
|
|
22
|
-
meta: {
|
|
23
|
-
description:
|
|
24
|
-
"Flags non-primitive objects interpolated into template literals — produces `[object Object]` unless `toString()` is overridden.",
|
|
25
|
-
examples: ["no-object-in-template: error"],
|
|
26
|
-
},
|
|
27
|
-
visit(node, ctx) {
|
|
28
|
-
if (!ts.isTemplateExpression(node)) return;
|
|
29
|
-
for (const span of node.templateSpans) {
|
|
30
|
-
const type = ctx.checker.getTypeAtLocation(span.expression);
|
|
31
|
-
if (wouldRenderAsObjectObject(type, ctx.checker)) {
|
|
32
|
-
ctx.report(
|
|
33
|
-
span.expression,
|
|
34
|
-
"Object interpolated in template literal has no custom toString() — it will render as [object Object]. Access a specific property or implement toString()."
|
|
35
|
-
);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
},
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
function wouldRenderAsObjectObject(type: ts.Type, checker: ts.TypeChecker): boolean {
|
|
42
|
-
const primitiveFlags =
|
|
43
|
-
ts.TypeFlags.String |
|
|
44
|
-
ts.TypeFlags.Number |
|
|
45
|
-
ts.TypeFlags.Boolean |
|
|
46
|
-
ts.TypeFlags.BigInt |
|
|
47
|
-
ts.TypeFlags.Null |
|
|
48
|
-
ts.TypeFlags.Undefined |
|
|
49
|
-
ts.TypeFlags.StringLiteral |
|
|
50
|
-
ts.TypeFlags.NumberLiteral |
|
|
51
|
-
ts.TypeFlags.BooleanLiteral |
|
|
52
|
-
ts.TypeFlags.BigIntLiteral |
|
|
53
|
-
ts.TypeFlags.Any |
|
|
54
|
-
ts.TypeFlags.Unknown |
|
|
55
|
-
ts.TypeFlags.Never |
|
|
56
|
-
ts.TypeFlags.Void |
|
|
57
|
-
ts.TypeFlags.ESSymbol |
|
|
58
|
-
ts.TypeFlags.UniqueESSymbol |
|
|
59
|
-
ts.TypeFlags.Enum |
|
|
60
|
-
ts.TypeFlags.EnumLiteral;
|
|
61
|
-
|
|
62
|
-
if (type.flags & primitiveFlags) return false;
|
|
63
|
-
|
|
64
|
-
if (type.isUnion()) {
|
|
65
|
-
const meaningful = type.types.filter(
|
|
66
|
-
(t) => !(t.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined))
|
|
67
|
-
);
|
|
68
|
-
return (
|
|
69
|
-
meaningful.length > 0 &&
|
|
70
|
-
meaningful.every((t) => wouldRenderAsObjectObject(t, checker))
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Functions coerce to their source string — unusual but not [object Object]
|
|
75
|
-
if (type.getCallSignatures().length > 0) return false;
|
|
76
|
-
|
|
77
|
-
// Known builtins with meaningful toString
|
|
78
|
-
const sym = type.getSymbol();
|
|
79
|
-
if (sym && SAFE_SYMBOL_NAMES.has(sym.name)) return false;
|
|
80
|
-
|
|
81
|
-
// Check for a custom toString not originating from TypeScript's built-in lib
|
|
82
|
-
const toStringProp = checker.getPropertyOfType(type, "toString");
|
|
83
|
-
if (!toStringProp) return true;
|
|
84
|
-
|
|
85
|
-
const decls = toStringProp.getDeclarations() ?? [];
|
|
86
|
-
const hasCustomToString = decls.some((d) => {
|
|
87
|
-
const fileName = d.getSourceFile().fileName;
|
|
88
|
-
return (
|
|
89
|
-
!fileName.includes("/typescript/lib/lib") &&
|
|
90
|
-
!fileName.includes(String.raw`\typescript\lib\lib`)
|
|
91
|
-
);
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
return !hasCustomToString;
|
|
95
|
-
}
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import ts from "typescript";
|
|
2
|
-
import { defineAstRule } from "../core/rule-helpers";
|
|
3
|
-
|
|
4
|
-
export const noOptionalChainOnNonNullable = defineAstRule({
|
|
5
|
-
meta: {
|
|
6
|
-
description:
|
|
7
|
-
"Flags `?.` on receivers whose static type cannot be `null` or `undefined`. The optional chain is dead code and obscures the real shape.",
|
|
8
|
-
examples: ["no-optional-chain-on-non-nullable: warn"],
|
|
9
|
-
},
|
|
10
|
-
shouldRun(program) {
|
|
11
|
-
return program.getCompilerOptions().strictNullChecks === true;
|
|
12
|
-
},
|
|
13
|
-
visit(node, ctx) {
|
|
14
|
-
const receiver = getOptionalChainReceiver(node);
|
|
15
|
-
if (!receiver) return;
|
|
16
|
-
const type = ctx.checker.getTypeAtLocation(receiver);
|
|
17
|
-
if (!isNullable(type)) {
|
|
18
|
-
ctx.report(
|
|
19
|
-
node,
|
|
20
|
-
"Optional chain (?.) on a non-nullable type — the receiver can never be null or undefined here. Use . to remove misleading dead code."
|
|
21
|
-
);
|
|
22
|
-
}
|
|
23
|
-
},
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
function getOptionalChainReceiver(node: ts.Node): ts.Expression | undefined {
|
|
27
|
-
if (
|
|
28
|
-
(ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node)) &&
|
|
29
|
-
node.questionDotToken
|
|
30
|
-
) {
|
|
31
|
-
return node.expression;
|
|
32
|
-
}
|
|
33
|
-
if (ts.isCallExpression(node) && node.questionDotToken) {
|
|
34
|
-
return node.expression;
|
|
35
|
-
}
|
|
36
|
-
return undefined;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function isNullable(type: ts.Type): boolean {
|
|
40
|
-
if (
|
|
41
|
-
type.flags &
|
|
42
|
-
(ts.TypeFlags.Null | ts.TypeFlags.Undefined | ts.TypeFlags.Any | ts.TypeFlags.Unknown)
|
|
43
|
-
)
|
|
44
|
-
return true;
|
|
45
|
-
if (type.isUnion()) return type.types.some(isNullable);
|
|
46
|
-
return false;
|
|
47
|
-
}
|
|
@@ -1,113 +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
|
-
/**
|
|
8
|
-
* Metacharacters that lose their special meaning inside a character class, so `[.]`
|
|
9
|
-
* is a valid shorthand for a literal dot and must not be unwrapped to a bare `.`.
|
|
10
|
-
*/
|
|
11
|
-
const METACHAR_EXCEPTIONS = new Set([".", "*", "+", "?", "{", "}", "(", ")", "|", "$"]);
|
|
12
|
-
|
|
13
|
-
interface CharClass {
|
|
14
|
-
bracketStart: number;
|
|
15
|
-
bracketEnd: number;
|
|
16
|
-
innerToken: string;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function parseCharClasses(pattern: string): CharClass[] {
|
|
20
|
-
const results: CharClass[] = [];
|
|
21
|
-
let i = 0;
|
|
22
|
-
while (i < pattern.length) {
|
|
23
|
-
if (pattern[i] === "\\") {
|
|
24
|
-
i += 2;
|
|
25
|
-
continue;
|
|
26
|
-
}
|
|
27
|
-
if (pattern[i] !== "[") {
|
|
28
|
-
i++;
|
|
29
|
-
continue;
|
|
30
|
-
}
|
|
31
|
-
const start = i++;
|
|
32
|
-
// Negated classes have different semantics — skip
|
|
33
|
-
if (i < pattern.length && pattern[i] === "^") {
|
|
34
|
-
while (i < pattern.length && pattern[i] !== "]") {
|
|
35
|
-
if (pattern[i] === "\\") i++;
|
|
36
|
-
i++;
|
|
37
|
-
}
|
|
38
|
-
i++;
|
|
39
|
-
continue;
|
|
40
|
-
}
|
|
41
|
-
const tokens: string[] = [];
|
|
42
|
-
while (i < pattern.length && pattern[i] !== "]") {
|
|
43
|
-
if (pattern[i] === "\\") {
|
|
44
|
-
const s = i++;
|
|
45
|
-
if (i >= pattern.length) break;
|
|
46
|
-
const c = pattern[i];
|
|
47
|
-
if (c === "u" && i + 5 <= pattern.length) {
|
|
48
|
-
i += 5; // \uXXXX
|
|
49
|
-
} else if (c === "x" && i + 3 <= pattern.length) {
|
|
50
|
-
i += 3; // \xXX
|
|
51
|
-
} else if (c === "c" && i + 1 < pattern.length) {
|
|
52
|
-
i += 2; // \cX
|
|
53
|
-
} else {
|
|
54
|
-
i++;
|
|
55
|
-
}
|
|
56
|
-
tokens.push(pattern.slice(s, i));
|
|
57
|
-
} else {
|
|
58
|
-
tokens.push(pattern[i++]);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
if (i >= pattern.length) break;
|
|
62
|
-
const end = i++;
|
|
63
|
-
if (tokens.length === 1) {
|
|
64
|
-
results.push({ bracketStart: start, bracketEnd: end, innerToken: tokens[0] });
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
return results;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export const noSingleCharClass: KlintRule = {
|
|
71
|
-
meta: {
|
|
72
|
-
description:
|
|
73
|
-
"Flags single-character character classes in regex (e.g. `/[a]/`) — drop the brackets; the regex matches the same.",
|
|
74
|
-
examples: ["sonar/no-single-char-class: warn"],
|
|
75
|
-
},
|
|
76
|
-
check({ files, root, fileContents }, violations) {
|
|
77
|
-
for (const file of files) {
|
|
78
|
-
const content = fileContents.get(file) ?? "";
|
|
79
|
-
walkAst(file, content, (node, src) => {
|
|
80
|
-
if (!ts.isRegularExpressionLiteral(node)) return;
|
|
81
|
-
|
|
82
|
-
const regexSrc = node.getText(src);
|
|
83
|
-
const lastSlash = regexSrc.lastIndexOf("/");
|
|
84
|
-
const pattern = regexSrc.slice(1, lastSlash);
|
|
85
|
-
const flags = regexSrc.slice(lastSlash + 1);
|
|
86
|
-
|
|
87
|
-
const allClasses = parseCharClasses(pattern);
|
|
88
|
-
const toFix = allClasses.filter((c) => !METACHAR_EXCEPTIONS.has(c.innerToken));
|
|
89
|
-
if (toFix.length === 0) return;
|
|
90
|
-
|
|
91
|
-
let fixedPattern = "";
|
|
92
|
-
let prev = 0;
|
|
93
|
-
for (const cls of allClasses) {
|
|
94
|
-
if (METACHAR_EXCEPTIONS.has(cls.innerToken)) continue;
|
|
95
|
-
fixedPattern += pattern.slice(prev, cls.bracketStart);
|
|
96
|
-
fixedPattern += cls.innerToken;
|
|
97
|
-
prev = cls.bracketEnd + 1;
|
|
98
|
-
}
|
|
99
|
-
fixedPattern += pattern.slice(prev);
|
|
100
|
-
|
|
101
|
-
const fixedRegex = `/${fixedPattern}/${flags}`;
|
|
102
|
-
const fix = buildNodeReplacementFix(src, node, fixedRegex);
|
|
103
|
-
|
|
104
|
-
violations.push({
|
|
105
|
-
file: relativeSlashPath(root, file),
|
|
106
|
-
line: fix.startLine,
|
|
107
|
-
message: `Character class [${toFix[0].innerToken}] contains a single element — remove the brackets.`,
|
|
108
|
-
fix,
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
},
|
|
113
|
-
};
|
package/rules/no-string-match.ts
DELETED
|
@@ -1,63 +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 noStringMatch: KlintRule = {
|
|
7
|
-
meta: {
|
|
8
|
-
description:
|
|
9
|
-
"Flags `String#match` — prefer `.matchAll()` for iteration or `.test()` for boolean checks. `.match()` returns different shapes depending on the `/g` flag.",
|
|
10
|
-
examples: ["no-string-match: 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 === "match" &&
|
|
20
|
-
node.arguments.length === 1 &&
|
|
21
|
-
ts.isRegularExpressionLiteral(node.arguments[0])
|
|
22
|
-
) {
|
|
23
|
-
const flags = regexFlags(node.arguments[0].text);
|
|
24
|
-
if (!flags.includes("g")) {
|
|
25
|
-
const strText = (
|
|
26
|
-
node.expression as ts.PropertyAccessExpression
|
|
27
|
-
).expression.getText(src);
|
|
28
|
-
const regexText = node.arguments[0].getText(src);
|
|
29
|
-
const { line: s } = src.getLineAndCharacterOfPosition(node.getStart());
|
|
30
|
-
const { line: e } = src.getLineAndCharacterOfPosition(node.getEnd());
|
|
31
|
-
const fix =
|
|
32
|
-
s === e
|
|
33
|
-
? (() => {
|
|
34
|
-
const lineStart = src.getPositionOfLineAndCharacter(s, 0);
|
|
35
|
-
const nlPos = src.text.indexOf("\n", lineStart);
|
|
36
|
-
const lineText = src.text.slice(
|
|
37
|
-
lineStart,
|
|
38
|
-
nlPos === -1 ? undefined : nlPos
|
|
39
|
-
);
|
|
40
|
-
const fixed = lineText.replace(
|
|
41
|
-
node.getText(src),
|
|
42
|
-
`new RegExp(${regexText}).exec(${strText})`
|
|
43
|
-
);
|
|
44
|
-
return { startLine: s + 1, endLine: e + 1, replacement: fixed };
|
|
45
|
-
})()
|
|
46
|
-
: undefined;
|
|
47
|
-
violations.push({
|
|
48
|
-
file: relativeSlashPath(root, file),
|
|
49
|
-
line: s + 1,
|
|
50
|
-
message: `Use RegExp.exec() instead of String.match() for non-global regexes — use new RegExp(${regexText}).exec(${strText}) instead.`,
|
|
51
|
-
fix,
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
},
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
function regexFlags(literal: string): string {
|
|
61
|
-
const last = literal.lastIndexOf("/");
|
|
62
|
-
return last > 0 ? literal.slice(last + 1) : "";
|
|
63
|
-
}
|