@qualety/typescript 0.1.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/LICENSE +21 -0
- package/dist/exhaustive-switch.d.ts +8 -0
- package/dist/exhaustive-switch.d.ts.map +1 -0
- package/dist/exhaustive-switch.js +198 -0
- package/dist/exhaustive-switch.js.map +1 -0
- package/dist/explicit-public-return-types.d.ts +8 -0
- package/dist/explicit-public-return-types.d.ts.map +1 -0
- package/dist/explicit-public-return-types.js +123 -0
- package/dist/explicit-public-return-types.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +70 -0
- package/dist/index.js.map +1 -0
- package/dist/narrowing.d.ts +36 -0
- package/dist/narrowing.d.ts.map +1 -0
- package/dist/narrowing.js +776 -0
- package/dist/narrowing.js.map +1 -0
- package/dist/no-constant-condition.d.ts +8 -0
- package/dist/no-constant-condition.d.ts.map +1 -0
- package/dist/no-constant-condition.js +59 -0
- package/dist/no-constant-condition.js.map +1 -0
- package/dist/no-empty-catch.d.ts +7 -0
- package/dist/no-empty-catch.d.ts.map +1 -0
- package/dist/no-empty-catch.js +23 -0
- package/dist/no-empty-catch.js.map +1 -0
- package/dist/no-export-star.d.ts +7 -0
- package/dist/no-export-star.d.ts.map +1 -0
- package/dist/no-export-star.js +30 -0
- package/dist/no-export-star.js.map +1 -0
- package/dist/no-floating-promises.d.ts +14 -0
- package/dist/no-floating-promises.d.ts.map +1 -0
- package/dist/no-floating-promises.js +129 -0
- package/dist/no-floating-promises.js.map +1 -0
- package/dist/no-misused-promises.d.ts +8 -0
- package/dist/no-misused-promises.d.ts.map +1 -0
- package/dist/no-misused-promises.js +188 -0
- package/dist/no-misused-promises.js.map +1 -0
- package/dist/no-non-null-assertion.d.ts +7 -0
- package/dist/no-non-null-assertion.d.ts.map +1 -0
- package/dist/no-non-null-assertion.js +24 -0
- package/dist/no-non-null-assertion.js.map +1 -0
- package/dist/no-public-any.d.ts +8 -0
- package/dist/no-public-any.d.ts.map +1 -0
- package/dist/no-public-any.js +92 -0
- package/dist/no-public-any.js.map +1 -0
- package/dist/no-unnecessary-abstraction.d.ts +8 -0
- package/dist/no-unnecessary-abstraction.d.ts.map +1 -0
- package/dist/no-unnecessary-abstraction.js +452 -0
- package/dist/no-unnecessary-abstraction.js.map +1 -0
- package/dist/no-unsafe-assertion.d.ts +7 -0
- package/dist/no-unsafe-assertion.d.ts.map +1 -0
- package/dist/no-unsafe-assertion.js +38 -0
- package/dist/no-unsafe-assertion.js.map +1 -0
- package/dist/parse-flow.d.ts +16 -0
- package/dist/parse-flow.d.ts.map +1 -0
- package/dist/parse-flow.js +136 -0
- package/dist/parse-flow.js.map +1 -0
- package/dist/public-exports-tested.d.ts +8 -0
- package/dist/public-exports-tested.d.ts.map +1 -0
- package/dist/public-exports-tested.js +170 -0
- package/dist/public-exports-tested.js.map +1 -0
- package/dist/ts-source.d.ts +5 -0
- package/dist/ts-source.d.ts.map +1 -0
- package/dist/ts-source.js +28 -0
- package/dist/ts-source.js.map +1 -0
- package/dist/type-narrowing-checks.d.ts +8 -0
- package/dist/type-narrowing-checks.d.ts.map +1 -0
- package/dist/type-narrowing-checks.js +99 -0
- package/dist/type-narrowing-checks.js.map +1 -0
- package/dist/zod-boundary.d.ts +8 -0
- package/dist/zod-boundary.d.ts.map +1 -0
- package/dist/zod-boundary.js +110 -0
- package/dist/zod-boundary.js.map +1 -0
- package/package.json +44 -0
- package/tsconfig/recommended.json +11 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type ArrowFunction, type CallExpression, type ElementAccessExpression, type FunctionDeclaration, type FunctionExpression, type MethodDeclaration, Node, type ObjectBindingPattern, type PropertyAccessExpression } from "ts-morph";
|
|
2
|
+
export type FunctionLike = FunctionDeclaration | FunctionExpression | ArrowFunction | MethodDeclaration;
|
|
3
|
+
export declare const BOUNDARY_NAMES: Set<string>;
|
|
4
|
+
export declare const BOUNDARY_PREFIX: RegExp;
|
|
5
|
+
export declare function isFunctionLike(node: Node): node is FunctionLike;
|
|
6
|
+
export declare function functionLikeName(node: FunctionLike): string | undefined;
|
|
7
|
+
export declare function unknownParamNames(node: FunctionLike): string[];
|
|
8
|
+
export declare function isPropertyParseCall(node: Node, names: ReadonlySet<string>, wantJson: boolean): node is CallExpression;
|
|
9
|
+
export declare function isSchemaParseCall(node: Node): node is CallExpression;
|
|
10
|
+
export declare function firstArgIdentifier(node: Node): string | undefined;
|
|
11
|
+
export declare function aliasesOf(fn: FunctionLike, root: string): Set<string>;
|
|
12
|
+
export declare function isPropertyUse(node: Node, names: ReadonlySet<string>): node is PropertyAccessExpression | ElementAccessExpression | ObjectBindingPattern;
|
|
13
|
+
export declare function isTypeofObjectGuard(node: Node, names: ReadonlySet<string>): boolean;
|
|
14
|
+
export declare function isHandGuardCall(node: Node, names: ReadonlySet<string>): boolean;
|
|
15
|
+
export declare function typeofTarget(node: Node): string | undefined;
|
|
16
|
+
//# sourceMappingURL=parse-flow.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse-flow.d.ts","sourceRoot":"","sources":["../src/parse-flow.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,uBAAuB,EAC5B,KAAK,mBAAmB,EACxB,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,IAAI,EACJ,KAAK,oBAAoB,EACzB,KAAK,wBAAwB,EAC9B,MAAM,UAAU,CAAC;AAElB,MAAM,MAAM,YAAY,GACpB,mBAAmB,GACnB,kBAAkB,GAClB,aAAa,GACb,iBAAiB,CAAC;AAEtB,eAAO,MAAM,cAAc,aAKzB,CAAC;AACH,eAAO,MAAM,eAAe,QAAwB,CAAC;AAErD,wBAAgB,cAAc,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,IAAI,YAAY,CAO/D;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,YAAY,GAAG,MAAM,GAAG,SAAS,CASvE;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,YAAY,GAAG,MAAM,EAAE,CAY9D;AAID,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,IAAI,EACV,KAAK,EAAE,WAAW,CAAC,MAAM,CAAC,EAC1B,QAAQ,EAAE,OAAO,GAChB,IAAI,IAAI,cAAc,CAWxB;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,IAAI,cAAc,CAEpE;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,GAAG,SAAS,CAMjE;AAED,wBAAgB,SAAS,CAAC,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAmBrE;AAED,wBAAgB,aAAa,CAC3B,IAAI,EAAE,IAAI,EACV,KAAK,EAAE,WAAW,CAAC,MAAM,CAAC,GACzB,IAAI,IAAI,wBAAwB,GAAG,uBAAuB,GAAG,oBAAoB,CAanF;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,WAAW,CAAC,MAAM,CAAC,GAAG,OAAO,CAanF;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,WAAW,CAAC,MAAM,CAAC,GAAG,OAAO,CAU/E;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,GAAG,SAAS,CAS3D"}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { Node, } from "ts-morph";
|
|
2
|
+
export const BOUNDARY_NAMES = new Set([
|
|
3
|
+
"validateConfig",
|
|
4
|
+
"readConfigFile",
|
|
5
|
+
"loadConfig",
|
|
6
|
+
"loadPlugin",
|
|
7
|
+
]);
|
|
8
|
+
export const BOUNDARY_PREFIX = /^(read|load|parse)/i;
|
|
9
|
+
export function isFunctionLike(node) {
|
|
10
|
+
return (Node.isFunctionDeclaration(node) ||
|
|
11
|
+
Node.isFunctionExpression(node) ||
|
|
12
|
+
Node.isArrowFunction(node) ||
|
|
13
|
+
Node.isMethodDeclaration(node));
|
|
14
|
+
}
|
|
15
|
+
export function functionLikeName(node) {
|
|
16
|
+
if (Node.isFunctionDeclaration(node) || Node.isMethodDeclaration(node)) {
|
|
17
|
+
return node.getName();
|
|
18
|
+
}
|
|
19
|
+
const parent = node.getParent();
|
|
20
|
+
if (Node.isVariableDeclaration(parent)) {
|
|
21
|
+
return parent.getName();
|
|
22
|
+
}
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
export function unknownParamNames(node) {
|
|
26
|
+
const names = [];
|
|
27
|
+
for (const param of node.getParameters()) {
|
|
28
|
+
const typeNode = param.getTypeNode();
|
|
29
|
+
if (typeNode !== undefined && typeNode.getText() === "unknown") {
|
|
30
|
+
const name = param.getName();
|
|
31
|
+
if (!name.startsWith("{") && !name.startsWith("[")) {
|
|
32
|
+
names.push(name);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return names;
|
|
37
|
+
}
|
|
38
|
+
const SCHEMA_PARSE_NAMES = new Set(["parse", "safeParse"]);
|
|
39
|
+
export function isPropertyParseCall(node, names, wantJson) {
|
|
40
|
+
if (!Node.isCallExpression(node)) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
const expr = node.getExpression();
|
|
44
|
+
if (!Node.isPropertyAccessExpression(expr) || !names.has(expr.getName())) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
const obj = expr.getExpression();
|
|
48
|
+
const isJson = Node.isIdentifier(obj) && obj.getText() === "JSON";
|
|
49
|
+
return wantJson === isJson;
|
|
50
|
+
}
|
|
51
|
+
export function isSchemaParseCall(node) {
|
|
52
|
+
return isPropertyParseCall(node, SCHEMA_PARSE_NAMES, false);
|
|
53
|
+
}
|
|
54
|
+
export function firstArgIdentifier(node) {
|
|
55
|
+
if (!Node.isCallExpression(node)) {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
const arg = node.getArguments()[0];
|
|
59
|
+
return arg !== undefined && Node.isIdentifier(arg) ? arg.getText() : undefined;
|
|
60
|
+
}
|
|
61
|
+
export function aliasesOf(fn, root) {
|
|
62
|
+
const names = new Set([root]);
|
|
63
|
+
let grew = true;
|
|
64
|
+
while (grew) {
|
|
65
|
+
grew = false;
|
|
66
|
+
fn.forEachDescendant((node) => {
|
|
67
|
+
if (!Node.isVariableDeclaration(node)) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const init = node.getInitializer();
|
|
71
|
+
if (init !== undefined && Node.isIdentifier(init) && names.has(init.getText())) {
|
|
72
|
+
if (!names.has(node.getName())) {
|
|
73
|
+
names.add(node.getName());
|
|
74
|
+
grew = true;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
return names;
|
|
80
|
+
}
|
|
81
|
+
export function isPropertyUse(node, names) {
|
|
82
|
+
if (Node.isPropertyAccessExpression(node) || Node.isElementAccessExpression(node)) {
|
|
83
|
+
const expr = node.getExpression();
|
|
84
|
+
return Node.isIdentifier(expr) && names.has(expr.getText());
|
|
85
|
+
}
|
|
86
|
+
if (Node.isObjectBindingPattern(node)) {
|
|
87
|
+
const parent = node.getParent();
|
|
88
|
+
if (Node.isVariableDeclaration(parent)) {
|
|
89
|
+
const init = parent.getInitializer();
|
|
90
|
+
return init !== undefined && Node.isIdentifier(init) && names.has(init.getText());
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
export function isTypeofObjectGuard(node, names) {
|
|
96
|
+
if (!Node.isBinaryExpression(node)) {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
const op = node.getOperatorToken().getText();
|
|
100
|
+
if (op !== "===" && op !== "==" && op !== "!==" && op !== "!=") {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
const left = node.getLeft();
|
|
104
|
+
const right = node.getRight();
|
|
105
|
+
const typeofOperand = typeofTarget(left) ?? typeofTarget(right);
|
|
106
|
+
const literal = stringLiteral(left) ?? stringLiteral(right);
|
|
107
|
+
return typeofOperand !== undefined && names.has(typeofOperand) && literal === "object";
|
|
108
|
+
}
|
|
109
|
+
export function isHandGuardCall(node, names) {
|
|
110
|
+
if (!Node.isCallExpression(node)) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
const expr = node.getExpression();
|
|
114
|
+
if (!Node.isIdentifier(expr) || !/^is[A-Z]/.test(expr.getText())) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
const arg = node.getArguments()[0];
|
|
118
|
+
return arg !== undefined && Node.isIdentifier(arg) && names.has(arg.getText());
|
|
119
|
+
}
|
|
120
|
+
export function typeofTarget(node) {
|
|
121
|
+
if (!Node.isTypeOfExpression(node)) {
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
let expr = node.getExpression();
|
|
125
|
+
while (Node.isParenthesizedExpression(expr)) {
|
|
126
|
+
expr = expr.getExpression();
|
|
127
|
+
}
|
|
128
|
+
return Node.isIdentifier(expr) ? expr.getText() : undefined;
|
|
129
|
+
}
|
|
130
|
+
function stringLiteral(node) {
|
|
131
|
+
if (Node.isStringLiteral(node) || Node.isNoSubstitutionTemplateLiteral(node)) {
|
|
132
|
+
return node.getLiteralText();
|
|
133
|
+
}
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=parse-flow.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse-flow.js","sourceRoot":"","sources":["../src/parse-flow.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type RuleContext } from "qualety";
|
|
2
|
+
export declare const publicExportsTested: {
|
|
3
|
+
meta: import("qualety").RuleMeta & {
|
|
4
|
+
requires?: readonly ["typescript"] | undefined;
|
|
5
|
+
};
|
|
6
|
+
create(context: RuleContext<readonly ["typescript"]>): void | import("qualety").RuleListener;
|
|
7
|
+
};
|
|
8
|
+
//# sourceMappingURL=public-exports-tested.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"public-exports-tested.d.ts","sourceRoot":"","sources":["../src/public-exports-tested.ts"],"names":[],"mappings":"AACA,OAAO,EAA0B,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AAcnE,eAAO,MAAM,mBAAmB;;;;;CA2C9B,CAAC"}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { dirname, extname, join, resolve } from "node:path";
|
|
2
|
+
import { defineRule } from "qualety";
|
|
3
|
+
import { Node, SourceFile, SyntaxKind } from "ts-morph";
|
|
4
|
+
const TS_EXTS = [".ts", ".tsx", ".mts", ".cts"];
|
|
5
|
+
const SWAP_EXTS = new Set([".js", ".jsx", ".mjs", ".cjs", ...TS_EXTS]);
|
|
6
|
+
export const publicExportsTested = defineRule({
|
|
7
|
+
meta: {
|
|
8
|
+
requires: ["typescript"],
|
|
9
|
+
docs: {
|
|
10
|
+
description: "Every public value export in included non-test sources must be referenced from a test path.",
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
create(context) {
|
|
14
|
+
const sources = context.getArtifact("typescript").sources;
|
|
15
|
+
const displayByAbs = mapDisplayPaths(context, sources);
|
|
16
|
+
const referenced = new Set();
|
|
17
|
+
const exported = [];
|
|
18
|
+
for (const [abs, unit] of sources) {
|
|
19
|
+
if (/\.d\.(?:ts|mts|cts)$/.test(posix(abs)) || !(unit instanceof SourceFile)) {
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
const normalized = posix(abs);
|
|
23
|
+
const base = normalized.slice(normalized.lastIndexOf("/") + 1);
|
|
24
|
+
if (/\.(?:test|spec)\./.test(base) || normalized.split("/").includes("__tests__")) {
|
|
25
|
+
collectReferences(unit, abs, sources, referenced);
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
const display = displayByAbs.get(abs) ?? posix(abs);
|
|
29
|
+
exported.push(...collectPublicExports(unit, abs, display));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const seen = new Set();
|
|
33
|
+
for (const item of exported) {
|
|
34
|
+
if (seen.has(item.key) || referenced.has(item.key)) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
seen.add(item.key);
|
|
38
|
+
context.report({
|
|
39
|
+
severity: "error",
|
|
40
|
+
file: item.file,
|
|
41
|
+
range: item.range,
|
|
42
|
+
message: `Public export "${item.name}" in ${item.display} is not referenced from a test file.`,
|
|
43
|
+
suggestion: `Import "${item.name}" from a test path (*.test.*, *.spec.*, or a __tests__/ directory).`,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
function mapDisplayPaths(context, sources) {
|
|
49
|
+
const cwd = context.getCwd();
|
|
50
|
+
const displayByAbs = new Map();
|
|
51
|
+
for (const name of context.getFiles()) {
|
|
52
|
+
const abs = resolve(cwd, name);
|
|
53
|
+
if (sources.has(abs)) {
|
|
54
|
+
displayByAbs.set(abs, name);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return displayByAbs;
|
|
58
|
+
}
|
|
59
|
+
function collectPublicExports(sf, abs, display) {
|
|
60
|
+
const result = [];
|
|
61
|
+
const push = (name, node) => {
|
|
62
|
+
const start = sf.getLineAndColumnAtPos(node.getStart());
|
|
63
|
+
const end = sf.getLineAndColumnAtPos(node.getEnd());
|
|
64
|
+
result.push({
|
|
65
|
+
key: `${abs}#${name}`,
|
|
66
|
+
name,
|
|
67
|
+
file: abs,
|
|
68
|
+
display,
|
|
69
|
+
range: {
|
|
70
|
+
start: { line: start.line, column: start.column },
|
|
71
|
+
end: { line: end.line, column: end.column },
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
};
|
|
75
|
+
for (const stmt of sf.getStatements()) {
|
|
76
|
+
collectStatementExport(stmt, push);
|
|
77
|
+
}
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
function collectStatementExport(stmt, push) {
|
|
81
|
+
if (Node.isExportDeclaration(stmt)) {
|
|
82
|
+
collectExportDeclaration(stmt, push);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (Node.isExportAssignment(stmt)) {
|
|
86
|
+
if (!stmt.isExportEquals()) {
|
|
87
|
+
push("default", defaultKeyword(stmt) ?? stmt);
|
|
88
|
+
}
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (collectNamedValueExport(stmt, push)) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (Node.isVariableStatement(stmt) && stmt.hasExportKeyword()) {
|
|
95
|
+
for (const decl of stmt.getDeclarations()) {
|
|
96
|
+
push(decl.getName(), decl.getNameNode() ?? decl);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function collectNamedValueExport(stmt, push) {
|
|
101
|
+
if (!Node.isFunctionDeclaration(stmt) &&
|
|
102
|
+
!Node.isClassDeclaration(stmt) &&
|
|
103
|
+
!Node.isEnumDeclaration(stmt)) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
if (!stmt.hasExportKeyword()) {
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
if (stmt.isDefaultExport()) {
|
|
110
|
+
push("default", defaultKeyword(stmt) ?? stmt.getNameNode() ?? stmt);
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
const nameNode = stmt.getNameNode();
|
|
114
|
+
const name = stmt.getName();
|
|
115
|
+
if (name !== undefined && nameNode !== undefined) {
|
|
116
|
+
push(name, nameNode);
|
|
117
|
+
}
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
function collectExportDeclaration(stmt, push) {
|
|
121
|
+
if (stmt.isTypeOnly() || stmt.isNamespaceExport()) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
for (const spec of stmt.getNamedExports()) {
|
|
125
|
+
if (spec.isTypeOnly()) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
push(spec.compilerNode.name.text, spec.getNameNode());
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function collectReferences(sf, fromFile, sources, referenced) {
|
|
132
|
+
for (const decl of sf.getImportDeclarations()) {
|
|
133
|
+
const specifier = decl.getModuleSpecifierValue();
|
|
134
|
+
const target = resolveImportTarget(fromFile, specifier, sources);
|
|
135
|
+
if (target === undefined) {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (decl.getDefaultImport() !== undefined) {
|
|
139
|
+
referenced.add(`${target}#default`);
|
|
140
|
+
}
|
|
141
|
+
for (const named of decl.getNamedImports()) {
|
|
142
|
+
referenced.add(`${target}#${named.getName()}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function resolveImportTarget(fromFile, specifier, sources) {
|
|
147
|
+
if (!specifier.startsWith(".")) {
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
const resolved = resolve(dirname(fromFile), specifier);
|
|
151
|
+
const ext = extname(resolved);
|
|
152
|
+
const stem = SWAP_EXTS.has(ext) ? resolved.slice(0, -ext.length) : resolved;
|
|
153
|
+
const paths = [resolved];
|
|
154
|
+
TS_EXTS.forEach((tsExt) => {
|
|
155
|
+
paths.push(stem + tsExt);
|
|
156
|
+
paths.push(join(resolved, `index${tsExt}`));
|
|
157
|
+
});
|
|
158
|
+
const found = paths.find((path) => sources.has(path) || sources.has(resolve(path)));
|
|
159
|
+
if (found === undefined) {
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
return sources.has(found) ? found : resolve(found);
|
|
163
|
+
}
|
|
164
|
+
function defaultKeyword(node) {
|
|
165
|
+
return node.getFirstChildByKind(SyntaxKind.DefaultKeyword);
|
|
166
|
+
}
|
|
167
|
+
function posix(filePath) {
|
|
168
|
+
return filePath.split("\\").join("/");
|
|
169
|
+
}
|
|
170
|
+
//# sourceMappingURL=public-exports-tested.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"public-exports-tested.js","sourceRoot":"","sources":["../src/public-exports-tested.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { RuleContext } from "qualety";
|
|
2
|
+
import { type Node, SourceFile } from "ts-morph";
|
|
3
|
+
export declare function walkTsSources(sources: ReadonlyMap<string, unknown>, visit: (sourceFile: SourceFile, file: string) => void): void;
|
|
4
|
+
export declare function reportAt(context: Pick<RuleContext, "report">, file: string, node: Node, message: string, suggestion: string): void;
|
|
5
|
+
//# sourceMappingURL=ts-source.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ts-source.d.ts","sourceRoot":"","sources":["../src/ts-source.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAC3C,OAAO,EAAE,KAAK,IAAI,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAEjD,wBAAgB,aAAa,CAC3B,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,OAAO,CAAC,EACrC,KAAK,EAAE,CAAC,UAAU,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,GACpD,IAAI,CAcN;AAED,wBAAgB,QAAQ,CACtB,OAAO,EAAE,IAAI,CAAC,WAAW,EAAE,QAAQ,CAAC,EACpC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,IAAI,EACV,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,GACjB,IAAI,CAYN"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { SourceFile } from "ts-morph";
|
|
2
|
+
export function walkTsSources(sources, visit) {
|
|
3
|
+
for (const [abs, unit] of sources) {
|
|
4
|
+
const path = abs.split("\\").join("/");
|
|
5
|
+
const base = path.slice(path.lastIndexOf("/") + 1);
|
|
6
|
+
if (!(unit instanceof SourceFile) ||
|
|
7
|
+
/\.d\.(?:ts|mts|cts)$/.test(base) ||
|
|
8
|
+
/\.(?:test|spec)\./.test(base) ||
|
|
9
|
+
path.split("/").includes("__tests__")) {
|
|
10
|
+
continue;
|
|
11
|
+
}
|
|
12
|
+
visit(unit, abs);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export function reportAt(context, file, node, message, suggestion) {
|
|
16
|
+
const sourceFile = node.getSourceFile();
|
|
17
|
+
context.report({
|
|
18
|
+
severity: "error",
|
|
19
|
+
file,
|
|
20
|
+
range: {
|
|
21
|
+
start: sourceFile.getLineAndColumnAtPos(node.getStart()),
|
|
22
|
+
end: sourceFile.getLineAndColumnAtPos(node.getEnd()),
|
|
23
|
+
},
|
|
24
|
+
message,
|
|
25
|
+
suggestion,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=ts-source.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ts-source.js","sourceRoot":"","sources":["../src/ts-source.ts"],"names":[],"mappings":"AACA,OAAO,EAAa,UAAU,EAAE,MAAM,UAAU,CAAC;AAEjD,MAAM,UAAU,aAAa,CAC3B,OAAqC,EACrC,KAAqD;IAErD,KAAK,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,IAAI,OAAO,EAAE,CAAC;QAClC,MAAM,IAAI,GAAG,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACvC,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;QACnD,IACE,CAAC,CAAC,IAAI,YAAY,UAAU,CAAC;YAC7B,sBAAsB,CAAC,IAAI,CAAC,IAAI,CAAC;YACjC,mBAAmB,CAAC,IAAI,CAAC,IAAI,CAAC;YAC9B,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,EACrC,CAAC;YACD,SAAS;QACX,CAAC;QACD,KAAK,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;IACnB,CAAC;AACH,CAAC;AAED,MAAM,UAAU,QAAQ,CACtB,OAAoC,EACpC,IAAY,EACZ,IAAU,EACV,OAAe,EACf,UAAkB;IAElB,MAAM,UAAU,GAAG,IAAI,CAAC,aAAa,EAAE,CAAC;IACxC,OAAO,CAAC,MAAM,CAAC;QACb,QAAQ,EAAE,OAAO;QACjB,IAAI;QACJ,KAAK,EAAE;YACL,KAAK,EAAE,UAAU,CAAC,qBAAqB,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC;YACxD,GAAG,EAAE,UAAU,CAAC,qBAAqB,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;SACrD;QACD,OAAO;QACP,UAAU;KACX,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type RuleContext } from "qualety";
|
|
2
|
+
export declare const typeNarrowingChecks: {
|
|
3
|
+
meta: import("qualety").RuleMeta & {
|
|
4
|
+
requires?: readonly string[] | undefined;
|
|
5
|
+
};
|
|
6
|
+
create(context: RuleContext<readonly string[]>): void | import("qualety").RuleListener;
|
|
7
|
+
};
|
|
8
|
+
//# sourceMappingURL=type-narrowing-checks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"type-narrowing-checks.d.ts","sourceRoot":"","sources":["../src/type-narrowing-checks.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AAoBvD,eAAO,MAAM,mBAAmB;;;;;CAS9B,CAAC"}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { defineRule } from "qualety";
|
|
2
|
+
import { Node } from "ts-morph";
|
|
3
|
+
import { bindFunctionScan, conditionLeaves, conditionNodes, diagnoseConstant, hasPriorParse, isStrictRefinement, shouldReportUnchanged, subjectIdentIn, truePathRoot, } from "./narrowing.js";
|
|
4
|
+
import { firstArgIdentifier, isPropertyUse, isSchemaParseCall } from "./parse-flow.js";
|
|
5
|
+
const SUGGESTION = "Delete the check, or use a type predicate / asserts / non-empty predicate / schema .data / a narrowed binding.";
|
|
6
|
+
export const typeNarrowingChecks = defineRule({
|
|
7
|
+
meta: {
|
|
8
|
+
requires: ["typescript"],
|
|
9
|
+
docs: {
|
|
10
|
+
description: "A runtime check on a value is legitimate only if TypeScript sees a strict refinement on the true path.",
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
create: bindFunctionScan(scanFunction),
|
|
14
|
+
});
|
|
15
|
+
function scanFunction(fn, file, sourceFile, context, reported) {
|
|
16
|
+
for (const cond of conditionNodes(fn)) {
|
|
17
|
+
if (diagnoseConstant(fn, cond, sourceFile) !== undefined) {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
const pathRoot = truePathRoot(cond);
|
|
21
|
+
if (pathRoot === undefined) {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
for (const cand of conditionLeaves(fn, cond)) {
|
|
25
|
+
considerLeaf(cand, cond, pathRoot, file, context, reported);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
fn.forEachDescendant((node) => {
|
|
29
|
+
considerRawAfterParse(fn, node, file, sourceFile, context, reported);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
function considerLeaf(cand, cond, pathRoot, file, context, reported) {
|
|
33
|
+
if (cand.kind === "schema-parse") {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const beforeNode = subjectIdentIn(cand.node, cand.subject);
|
|
37
|
+
const afterNode = subjectIdentIn(pathRoot, cand.subject);
|
|
38
|
+
if (beforeNode === undefined || afterNode === undefined) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const before = beforeNode.getType();
|
|
42
|
+
const after = afterNode.getType();
|
|
43
|
+
if (isStrictRefinement(before, after) || !shouldReportUnchanged(cand, before, after)) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
emitA(context, file, cond, cand.subject, reported);
|
|
47
|
+
}
|
|
48
|
+
function considerRawAfterParse(fn, node, file, sourceFile, context, reported) {
|
|
49
|
+
if (!isSchemaParseCall(node)) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const subject = firstArgIdentifier(node);
|
|
53
|
+
if (subject === undefined || hasPriorParse(fn, subject, node.getStart())) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (diagnoseConstant(fn, node, sourceFile) !== undefined) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
let rawUse;
|
|
60
|
+
fn.forEachDescendant((child) => {
|
|
61
|
+
if (child.getStart() > node.getStart() && isPropertyUse(child, new Set([subject]))) {
|
|
62
|
+
rawUse = rawUse ?? child;
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
if (rawUse === undefined || refinedAfterParse(node, rawUse, subject)) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
emitA(context, file, node, subject, reported);
|
|
69
|
+
}
|
|
70
|
+
function refinedAfterParse(parseNode, rawUse, subject) {
|
|
71
|
+
const beforeNode = subjectIdentIn(parseNode, subject);
|
|
72
|
+
const afterNode = Node.isPropertyAccessExpression(rawUse) || Node.isElementAccessExpression(rawUse)
|
|
73
|
+
? rawUse.getExpression()
|
|
74
|
+
: subjectIdentIn(rawUse, subject);
|
|
75
|
+
if (beforeNode === undefined || afterNode === undefined) {
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
return isStrictRefinement(beforeNode.getType(), afterNode.getType());
|
|
79
|
+
}
|
|
80
|
+
function emitA(context, file, node, subject, reported) {
|
|
81
|
+
const sourceFile = node.getSourceFile();
|
|
82
|
+
const range = {
|
|
83
|
+
start: sourceFile.getLineAndColumnAtPos(node.getStart()),
|
|
84
|
+
end: sourceFile.getLineAndColumnAtPos(node.getEnd()),
|
|
85
|
+
};
|
|
86
|
+
const key = `${file}:${range.start.line}:${range.start.column}`;
|
|
87
|
+
if (reported.has(key)) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
reported.add(key);
|
|
91
|
+
context.report({
|
|
92
|
+
severity: "error",
|
|
93
|
+
file,
|
|
94
|
+
range,
|
|
95
|
+
message: `Check on "${subject}" does not narrow its type.`,
|
|
96
|
+
suggestion: SUGGESTION,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=type-narrowing-checks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"type-narrowing-checks.js","sourceRoot":"","sources":["../src/type-narrowing-checks.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type RuleContext } from "qualety";
|
|
2
|
+
export declare const zodBoundary: {
|
|
3
|
+
meta: import("qualety").RuleMeta & {
|
|
4
|
+
requires?: readonly ["typescript"] | undefined;
|
|
5
|
+
};
|
|
6
|
+
create(context: RuleContext<readonly ["typescript"]>): void | import("qualety").RuleListener;
|
|
7
|
+
};
|
|
8
|
+
//# sourceMappingURL=zod-boundary.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"zod-boundary.d.ts","sourceRoot":"","sources":["../src/zod-boundary.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AAqBvD,eAAO,MAAM,WAAW;;;;;CAwBtB,CAAC"}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { defineRule } from "qualety";
|
|
2
|
+
import { Node, SourceFile } from "ts-morph";
|
|
3
|
+
import { enclosingFunction } from "./narrowing.js";
|
|
4
|
+
import { aliasesOf, BOUNDARY_NAMES, BOUNDARY_PREFIX, firstArgIdentifier, functionLikeName, isFunctionLike, isPropertyParseCall, isPropertyUse, isSchemaParseCall, unknownParamNames, } from "./parse-flow.js";
|
|
5
|
+
const PARSE_SUGGESTION = "Call schema.safeParse(...) or schema.parse(...) on this value before reading its properties.";
|
|
6
|
+
const JSON_PARSE_NAMES = new Set(["parse"]);
|
|
7
|
+
export const zodBoundary = defineRule({
|
|
8
|
+
meta: {
|
|
9
|
+
requires: ["typescript"],
|
|
10
|
+
docs: {
|
|
11
|
+
description: "Load/parse boundaries typed unknown and JSON.parse results must be schema-parsed before property access.",
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
create(context) {
|
|
15
|
+
const sources = context.getArtifact("typescript").sources;
|
|
16
|
+
for (const [abs, unit] of sources) {
|
|
17
|
+
if (!(unit instanceof SourceFile)) {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
unit.forEachDescendant((node) => {
|
|
21
|
+
if (isFunctionLike(node)) {
|
|
22
|
+
scanBoundary(node, abs, context);
|
|
23
|
+
}
|
|
24
|
+
if (isPropertyParseCall(node, JSON_PARSE_NAMES, true)) {
|
|
25
|
+
scanJsonParse(node, abs, context);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
function scanBoundary(fn, file, context) {
|
|
32
|
+
const name = functionLikeName(fn);
|
|
33
|
+
if (name === undefined || !(BOUNDARY_NAMES.has(name) || BOUNDARY_PREFIX.test(name))) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
for (const param of unknownParamNames(fn)) {
|
|
37
|
+
reportUsesBeforeParse(fn, aliasesOf(fn, param), file, context, `Load/parse function "${name}" uses untrusted parameter "${param}" before schema.parse/safeParse.`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function scanJsonParse(node, file, context) {
|
|
41
|
+
const parseParent = node.getParent();
|
|
42
|
+
if (parseParent !== undefined &&
|
|
43
|
+
isSchemaParseCall(parseParent) &&
|
|
44
|
+
parseParent.getArguments().includes(node)) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const parent = node.getParent();
|
|
48
|
+
if (parent !== undefined &&
|
|
49
|
+
(Node.isPropertyAccessExpression(parent) || Node.isElementAccessExpression(parent))) {
|
|
50
|
+
context.report({
|
|
51
|
+
severity: "error",
|
|
52
|
+
file,
|
|
53
|
+
range: {
|
|
54
|
+
start: parent.getSourceFile().getLineAndColumnAtPos(parent.getStart()),
|
|
55
|
+
end: parent.getSourceFile().getLineAndColumnAtPos(parent.getEnd()),
|
|
56
|
+
},
|
|
57
|
+
message: "JSON.parse result is used before schema.parse/safeParse.",
|
|
58
|
+
suggestion: PARSE_SUGGESTION,
|
|
59
|
+
});
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (!Node.isVariableDeclaration(parent) || !Node.isCallExpression(node)) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const binding = parent.getName();
|
|
66
|
+
const fn = enclosingFunction(node);
|
|
67
|
+
if (fn === undefined) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
reportUsesBeforeParse(fn, aliasesOf(fn, binding), file, context, "JSON.parse result is used before schema.parse/safeParse.");
|
|
71
|
+
}
|
|
72
|
+
function reportUsesBeforeParse(fn, names, file, context, message) {
|
|
73
|
+
const parseAt = firstParsePos(fn, names);
|
|
74
|
+
for (const node of fn.getDescendants()) {
|
|
75
|
+
if (!isPropertyUse(node, names)) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (parseAt !== undefined && node.getStart() > parseAt) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
context.report({
|
|
82
|
+
severity: "error",
|
|
83
|
+
file,
|
|
84
|
+
range: {
|
|
85
|
+
start: node.getSourceFile().getLineAndColumnAtPos(node.getStart()),
|
|
86
|
+
end: node.getSourceFile().getLineAndColumnAtPos(node.getEnd()),
|
|
87
|
+
},
|
|
88
|
+
message,
|
|
89
|
+
suggestion: PARSE_SUGGESTION,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function firstParsePos(fn, names) {
|
|
94
|
+
let pos;
|
|
95
|
+
for (const node of fn.getDescendants()) {
|
|
96
|
+
if (!isSchemaParseCall(node)) {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const arg = firstArgIdentifier(node);
|
|
100
|
+
if (arg === undefined || !names.has(arg)) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const start = node.getStart();
|
|
104
|
+
if (pos === undefined || start < pos) {
|
|
105
|
+
pos = start;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return pos;
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=zod-boundary.js.map
|