@antithrow/eslint-plugin 1.1.0 → 1.2.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 +3 -1
- package/dist/create-rule.js +1 -1
- package/dist/index.js +3 -1
- package/dist/rules/index.d.ts +1 -0
- package/dist/rules/index.js +1 -0
- package/dist/rules/no-throwing-call.d.ts +9 -0
- package/dist/rules/no-throwing-call.js +172 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
<h1>@antithrow/eslint-plugin</h1>
|
|
3
3
|
<p>
|
|
4
|
-
ESLint rules for <a href="https://github.com/
|
|
4
|
+
ESLint rules for <a href="https://github.com/antithrow/antithrow">antithrow</a> Result types
|
|
5
5
|
</p>
|
|
6
6
|
|
|
7
7
|

|
|
@@ -40,6 +40,7 @@ export default [
|
|
|
40
40
|
"@antithrow": antithrow,
|
|
41
41
|
},
|
|
42
42
|
rules: {
|
|
43
|
+
"@antithrow/no-throwing-call": "warn",
|
|
43
44
|
"@antithrow/no-unsafe-unwrap": "warn",
|
|
44
45
|
"@antithrow/no-unused-result": "error",
|
|
45
46
|
},
|
|
@@ -51,5 +52,6 @@ export default [
|
|
|
51
52
|
|
|
52
53
|
| Rule | Description | Recommended |
|
|
53
54
|
| --- | --- | --- |
|
|
55
|
+
| [`no-throwing-call`](./docs/rules/no-throwing-call.md) | Disallow calls to throwing built-in APIs with `@antithrow/std` replacements | `warn` |
|
|
54
56
|
| [`no-unsafe-unwrap`](./docs/rules/no-unsafe-unwrap.md) | Disallow `unwrap`/`expect` APIs on antithrow `Result` values | `warn` |
|
|
55
57
|
| [`no-unused-result`](./docs/rules/no-unused-result.md) | Require `Result` and `ResultAsync` values to be used | `error` |
|
package/dist/create-rule.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import { ESLintUtils } from "@typescript-eslint/utils";
|
|
2
|
-
export const createRule = ESLintUtils.RuleCreator((name) => `https://github.com/
|
|
2
|
+
export const createRule = ESLintUtils.RuleCreator((name) => `https://github.com/antithrow/antithrow/blob/main/packages/eslint-plugin/docs/rules/${name}.md`);
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import packageJson from "../package.json" with { type: "json" };
|
|
2
|
-
import { noUnsafeUnwrap, noUnusedResult } from "./rules/index.js";
|
|
2
|
+
import { noThrowingCall, noUnsafeUnwrap, noUnusedResult } from "./rules/index.js";
|
|
3
3
|
const plugin = {
|
|
4
4
|
meta: {
|
|
5
5
|
name: packageJson.name,
|
|
6
6
|
version: packageJson.version,
|
|
7
7
|
},
|
|
8
8
|
rules: {
|
|
9
|
+
"no-throwing-call": noThrowingCall,
|
|
9
10
|
"no-unsafe-unwrap": noUnsafeUnwrap,
|
|
10
11
|
"no-unused-result": noUnusedResult,
|
|
11
12
|
},
|
|
@@ -18,6 +19,7 @@ plugin.configs = {
|
|
|
18
19
|
"@antithrow": plugin,
|
|
19
20
|
},
|
|
20
21
|
rules: {
|
|
22
|
+
"@antithrow/no-throwing-call": "warn",
|
|
21
23
|
"@antithrow/no-unsafe-unwrap": "warn",
|
|
22
24
|
"@antithrow/no-unused-result": "error",
|
|
23
25
|
},
|
package/dist/rules/index.d.ts
CHANGED
package/dist/rules/index.js
CHANGED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ESLintUtils } from "@typescript-eslint/utils";
|
|
2
|
+
/** @lintignore */
|
|
3
|
+
export declare const MessageId: {
|
|
4
|
+
readonly THROWING_CALL: "throwingCall";
|
|
5
|
+
};
|
|
6
|
+
export type MessageId = (typeof MessageId)[keyof typeof MessageId];
|
|
7
|
+
export declare const noThrowingCall: ESLintUtils.RuleModule<"throwingCall", [], import("../create-rule.js").AntithrowPluginDocs, ESLintUtils.RuleListener> & {
|
|
8
|
+
name: string;
|
|
9
|
+
};
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { ESLintUtils } from "@typescript-eslint/utils";
|
|
2
|
+
import ts from "typescript";
|
|
3
|
+
import { createRule } from "../create-rule.js";
|
|
4
|
+
/** @lintignore */
|
|
5
|
+
export const MessageId = {
|
|
6
|
+
THROWING_CALL: "throwingCall",
|
|
7
|
+
};
|
|
8
|
+
const GLOBAL_FUNCTIONS = new Set([
|
|
9
|
+
"fetch",
|
|
10
|
+
"atob",
|
|
11
|
+
"btoa",
|
|
12
|
+
"structuredClone",
|
|
13
|
+
"decodeURI",
|
|
14
|
+
"decodeURIComponent",
|
|
15
|
+
"encodeURI",
|
|
16
|
+
"encodeURIComponent",
|
|
17
|
+
]);
|
|
18
|
+
const JSON_METHODS = new Set(["parse", "stringify"]);
|
|
19
|
+
const RESPONSE_BODY_METHODS = new Set(["json", "text", "arrayBuffer", "blob", "formData"]);
|
|
20
|
+
const GLOBAL_OBJECTS = new Set(["globalThis", "window", "self"]);
|
|
21
|
+
const NULLISH_TYPE_FLAGS = ts.TypeFlags.Null | ts.TypeFlags.Undefined | ts.TypeFlags.Void;
|
|
22
|
+
/**
|
|
23
|
+
* Extracts the property name from a `MemberExpression` when it can be
|
|
24
|
+
* statically determined. Handles `obj.prop`, `obj["prop"]`, and
|
|
25
|
+
* `` obj[`prop`] `` (template literals with no interpolations).
|
|
26
|
+
* Returns `null` for dynamic access like `obj[variable]`.
|
|
27
|
+
*/
|
|
28
|
+
function getStaticMemberName(node) {
|
|
29
|
+
if (!node.computed && node.property.type === "Identifier") {
|
|
30
|
+
return node.property.name;
|
|
31
|
+
}
|
|
32
|
+
if (node.computed &&
|
|
33
|
+
node.property.type === "Literal" &&
|
|
34
|
+
typeof node.property.value === "string") {
|
|
35
|
+
return node.property.value;
|
|
36
|
+
}
|
|
37
|
+
if (node.computed &&
|
|
38
|
+
node.property.type === "TemplateLiteral" &&
|
|
39
|
+
node.property.expressions.length === 0) {
|
|
40
|
+
const [quasi] = node.property.quasis;
|
|
41
|
+
return quasi?.value.cooked ?? null;
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
function isImplicitGlobal(name, scope) {
|
|
46
|
+
let current = scope;
|
|
47
|
+
while (current) {
|
|
48
|
+
const variable = current.set.get(name);
|
|
49
|
+
if (variable) {
|
|
50
|
+
return variable.defs.length === 0;
|
|
51
|
+
}
|
|
52
|
+
current = current.upper;
|
|
53
|
+
}
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
function isImplicitGlobalObject(node, scope) {
|
|
57
|
+
return GLOBAL_OBJECTS.has(node.name) && isImplicitGlobal(node.name, scope);
|
|
58
|
+
}
|
|
59
|
+
function containsGlobalResponseType(type, globalResponseType, checker) {
|
|
60
|
+
if (type.isUnionOrIntersection()) {
|
|
61
|
+
return type.types.some((member) => containsGlobalResponseType(member, globalResponseType, checker));
|
|
62
|
+
}
|
|
63
|
+
const flags = type.getFlags();
|
|
64
|
+
if (flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown | ts.TypeFlags.Never | NULLISH_TYPE_FLAGS)) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
if (checker.isTypeAssignableTo(type, globalResponseType)) {
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
const constraint = checker.getBaseConstraintOfType(type);
|
|
71
|
+
if (constraint && constraint !== type) {
|
|
72
|
+
return containsGlobalResponseType(constraint, globalResponseType, checker);
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
export const noThrowingCall = createRule({
|
|
77
|
+
name: "no-throwing-call",
|
|
78
|
+
meta: {
|
|
79
|
+
type: "problem",
|
|
80
|
+
docs: {
|
|
81
|
+
description: "Disallow calls to known throwing built-in APIs that have @antithrow/std replacements.",
|
|
82
|
+
recommended: true,
|
|
83
|
+
requiresTypeChecking: true,
|
|
84
|
+
},
|
|
85
|
+
messages: {
|
|
86
|
+
[MessageId.THROWING_CALL]: "`{{ api }}` can throw. A non-throwing wrapper is available from `@antithrow/std`.",
|
|
87
|
+
},
|
|
88
|
+
schema: [],
|
|
89
|
+
},
|
|
90
|
+
defaultOptions: [],
|
|
91
|
+
create(context) {
|
|
92
|
+
const services = ESLintUtils.getParserServices(context);
|
|
93
|
+
const checker = services.program.getTypeChecker();
|
|
94
|
+
const globalResponseSymbol = checker.resolveName("Response", undefined, ts.SymbolFlags.Type, false);
|
|
95
|
+
const globalResponseType = globalResponseSymbol
|
|
96
|
+
? checker.getDeclaredTypeOfSymbol(globalResponseSymbol)
|
|
97
|
+
: null;
|
|
98
|
+
return {
|
|
99
|
+
CallExpression(node) {
|
|
100
|
+
const callee = node.callee;
|
|
101
|
+
// fetch(), atob(), etc.
|
|
102
|
+
if (callee.type === "Identifier" && GLOBAL_FUNCTIONS.has(callee.name)) {
|
|
103
|
+
const scope = context.sourceCode.getScope(callee);
|
|
104
|
+
if (isImplicitGlobal(callee.name, scope)) {
|
|
105
|
+
context.report({
|
|
106
|
+
node,
|
|
107
|
+
messageId: MessageId.THROWING_CALL,
|
|
108
|
+
data: { api: callee.name },
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (callee.type !== "MemberExpression") {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const methodName = getStaticMemberName(callee);
|
|
117
|
+
if (!methodName) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
// globalThis.fetch(), window.atob(), self.structuredClone(), etc.
|
|
121
|
+
if (GLOBAL_FUNCTIONS.has(methodName) &&
|
|
122
|
+
callee.object.type === "Identifier" &&
|
|
123
|
+
isImplicitGlobalObject(callee.object, context.sourceCode.getScope(callee.object))) {
|
|
124
|
+
context.report({
|
|
125
|
+
node,
|
|
126
|
+
messageId: MessageId.THROWING_CALL,
|
|
127
|
+
data: { api: methodName },
|
|
128
|
+
});
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
// JSON.parse(), JSON.stringify()
|
|
132
|
+
if (JSON_METHODS.has(methodName) &&
|
|
133
|
+
callee.object.type === "Identifier" &&
|
|
134
|
+
callee.object.name === "JSON") {
|
|
135
|
+
const scope = context.sourceCode.getScope(callee.object);
|
|
136
|
+
if (isImplicitGlobal("JSON", scope)) {
|
|
137
|
+
context.report({
|
|
138
|
+
node,
|
|
139
|
+
messageId: MessageId.THROWING_CALL,
|
|
140
|
+
data: { api: `JSON.${methodName}` },
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
// globalThis.JSON.parse(), window.JSON.stringify(), etc.
|
|
146
|
+
if (JSON_METHODS.has(methodName) &&
|
|
147
|
+
callee.object.type === "MemberExpression" &&
|
|
148
|
+
getStaticMemberName(callee.object) === "JSON" &&
|
|
149
|
+
callee.object.object.type === "Identifier" &&
|
|
150
|
+
isImplicitGlobalObject(callee.object.object, context.sourceCode.getScope(callee.object.object))) {
|
|
151
|
+
context.report({
|
|
152
|
+
node,
|
|
153
|
+
messageId: MessageId.THROWING_CALL,
|
|
154
|
+
data: { api: `JSON.${methodName}` },
|
|
155
|
+
});
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (RESPONSE_BODY_METHODS.has(methodName) && globalResponseType) {
|
|
159
|
+
const tsNode = services.esTreeNodeToTSNodeMap.get(callee.object);
|
|
160
|
+
const type = checker.getTypeAtLocation(tsNode);
|
|
161
|
+
if (containsGlobalResponseType(type, globalResponseType, checker)) {
|
|
162
|
+
context.report({
|
|
163
|
+
node,
|
|
164
|
+
messageId: MessageId.THROWING_CALL,
|
|
165
|
+
data: { api: `response.${methodName}` },
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
},
|
|
172
|
+
});
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@antithrow/eslint-plugin",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "ESLint plugin for antithrow Result types",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Jack Weilage",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
|
-
"url": "git+https://github.com/
|
|
9
|
+
"url": "git+https://github.com/antithrow/antithrow.git",
|
|
10
10
|
"directory": "packages/eslint-plugin"
|
|
11
11
|
},
|
|
12
12
|
"keywords": [
|