@antithrow/eslint-plugin 1.2.0 → 1.2.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 CHANGED
@@ -6,6 +6,7 @@
6
6
 
7
7
  ![NPM Version](https://img.shields.io/npm/v/@antithrow/eslint-plugin)
8
8
  ![NPM License](https://img.shields.io/npm/l/@antithrow/eslint-plugin)
9
+
9
10
  </div>
10
11
 
11
12
  ## Installation
@@ -50,8 +51,8 @@ export default [
50
51
 
51
52
  ## Rules
52
53
 
53
- | Rule | Description | Recommended |
54
- | --- | --- | --- |
55
- | [`no-throwing-call`](./docs/rules/no-throwing-call.md) | Disallow calls to throwing built-in APIs with `@antithrow/std` replacements | `warn` |
56
- | [`no-unsafe-unwrap`](./docs/rules/no-unsafe-unwrap.md) | Disallow `unwrap`/`expect` APIs on antithrow `Result` values | `warn` |
57
- | [`no-unused-result`](./docs/rules/no-unused-result.md) | Require `Result` and `ResultAsync` values to be used | `error` |
54
+ | Rule | Description | Recommended |
55
+ | ------------------------------------------------------ | --------------------------------------------------------------------------- | ----------- |
56
+ | [`no-throwing-call`](https://antithrow.dev/docs/api/eslint-plugin/no-throwing-call) | Disallow calls to throwing built-in APIs with `@antithrow/std` replacements | `warn` |
57
+ | [`no-unsafe-unwrap`](https://antithrow.dev/docs/api/eslint-plugin/no-unsafe-unwrap) | Disallow `unwrap`/`expect` APIs on antithrow `Result` values | `warn` |
58
+ | [`no-unused-result`](https://antithrow.dev/docs/api/eslint-plugin/no-unused-result) | Require `Result` and `ResultAsync` values to be used | `error` |
@@ -1,2 +1,2 @@
1
1
  import { ESLintUtils } from "@typescript-eslint/utils";
2
- export const createRule = ESLintUtils.RuleCreator((name) => `https://github.com/antithrow/antithrow/blob/main/packages/eslint-plugin/docs/rules/${name}.md`);
2
+ export const createRule = ESLintUtils.RuleCreator((name) => `https://antithrow.dev/docs/api/eslint-plugin/${name}`);
@@ -5,7 +5,7 @@ import { createRule } from "../create-rule.js";
5
5
  export const MessageId = {
6
6
  THROWING_CALL: "throwingCall",
7
7
  };
8
- const GLOBAL_FUNCTIONS = new Set([
8
+ const BANNED_GLOBAL_CALLS = new Set([
9
9
  "fetch",
10
10
  "atob",
11
11
  "btoa",
@@ -14,8 +14,9 @@ const GLOBAL_FUNCTIONS = new Set([
14
14
  "decodeURIComponent",
15
15
  "encodeURI",
16
16
  "encodeURIComponent",
17
+ "JSON.parse",
18
+ "JSON.stringify",
17
19
  ]);
18
- const JSON_METHODS = new Set(["parse", "stringify"]);
19
20
  const RESPONSE_BODY_METHODS = new Set(["json", "text", "arrayBuffer", "blob", "formData"]);
20
21
  const GLOBAL_OBJECTS = new Set(["globalThis", "window", "self"]);
21
22
  const NULLISH_TYPE_FLAGS = ts.TypeFlags.Null | ts.TypeFlags.Undefined | ts.TypeFlags.Void;
@@ -53,8 +54,63 @@ function isImplicitGlobal(name, scope) {
53
54
  }
54
55
  return true;
55
56
  }
56
- function isImplicitGlobalObject(node, scope) {
57
- return GLOBAL_OBJECTS.has(node.name) && isImplicitGlobal(node.name, scope);
57
+ function collectStaticMemberPath(node) {
58
+ const propertyName = getStaticMemberName(node);
59
+ if (!propertyName) {
60
+ return null;
61
+ }
62
+ if (node.object.type === "Identifier") {
63
+ return {
64
+ segments: [node.object.name, propertyName],
65
+ rootIdentifier: node.object,
66
+ };
67
+ }
68
+ if (node.object.type !== "MemberExpression") {
69
+ return null;
70
+ }
71
+ const parentPath = collectStaticMemberPath(node.object);
72
+ if (!parentPath) {
73
+ return null;
74
+ }
75
+ return {
76
+ segments: [...parentPath.segments, propertyName],
77
+ rootIdentifier: parentPath.rootIdentifier,
78
+ };
79
+ }
80
+ function getStaticCalleePath(callee) {
81
+ if (callee.type === "Identifier") {
82
+ return {
83
+ segments: [callee.name],
84
+ rootIdentifier: callee,
85
+ memberExpression: null,
86
+ };
87
+ }
88
+ if (callee.type !== "MemberExpression") {
89
+ return null;
90
+ }
91
+ const memberPath = collectStaticMemberPath(callee);
92
+ if (!memberPath) {
93
+ return null;
94
+ }
95
+ return {
96
+ segments: memberPath.segments,
97
+ rootIdentifier: memberPath.rootIdentifier,
98
+ memberExpression: callee,
99
+ };
100
+ }
101
+ function normalizeGlobalPath(calleePath, scope) {
102
+ // Invariant: getStaticCalleePath always returns at least one segment.
103
+ const [root = "", ...rest] = calleePath.segments;
104
+ if (GLOBAL_OBJECTS.has(root)) {
105
+ if (!isImplicitGlobal(root, scope)) {
106
+ return null;
107
+ }
108
+ return rest.length > 0 ? rest : null;
109
+ }
110
+ if (!isImplicitGlobal(root, scope)) {
111
+ return null;
112
+ }
113
+ return calleePath.segments;
58
114
  }
59
115
  function containsGlobalResponseType(type, globalResponseType, checker) {
60
116
  if (type.isUnionOrIntersection()) {
@@ -97,66 +153,30 @@ export const noThrowingCall = createRule({
97
153
  : null;
98
154
  return {
99
155
  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") {
156
+ const calleePath = getStaticCalleePath(node.callee);
157
+ if (!calleePath) {
114
158
  return;
115
159
  }
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)) {
160
+ const rootScope = context.sourceCode.getScope(calleePath.rootIdentifier);
161
+ const normalizedGlobalPath = normalizeGlobalPath(calleePath, rootScope);
162
+ if (normalizedGlobalPath) {
163
+ const api = normalizedGlobalPath.join(".");
164
+ if (BANNED_GLOBAL_CALLS.has(api)) {
137
165
  context.report({
138
166
  node,
139
167
  messageId: MessageId.THROWING_CALL,
140
- data: { api: `JSON.${methodName}` },
168
+ data: { api },
141
169
  });
170
+ return;
142
171
  }
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
172
  }
158
- if (RESPONSE_BODY_METHODS.has(methodName) && globalResponseType) {
159
- const tsNode = services.esTreeNodeToTSNodeMap.get(callee.object);
173
+ const methodName = calleePath.segments[calleePath.segments.length - 1];
174
+ const { memberExpression } = calleePath;
175
+ if (methodName &&
176
+ memberExpression &&
177
+ RESPONSE_BODY_METHODS.has(methodName) &&
178
+ globalResponseType) {
179
+ const tsNode = services.esTreeNodeToTSNodeMap.get(memberExpression.object);
160
180
  const type = checker.getTypeAtLocation(tsNode);
161
181
  if (containsGlobalResponseType(type, globalResponseType, checker)) {
162
182
  context.report({
@@ -100,7 +100,7 @@ export const noUnsafeUnwrap = createRule({
100
100
  return {
101
101
  MemberExpression(node) {
102
102
  const method = getStaticMemberName(node);
103
- if (!method || !BANNED_METHOD_NAMES.has(method)) {
103
+ if (!(method && BANNED_METHOD_NAMES.has(method))) {
104
104
  return;
105
105
  }
106
106
  const tsNode = services.esTreeNodeToTSNodeMap.get(node.object);
@@ -153,7 +153,7 @@ export const noUnsafeUnwrap = createRule({
153
153
  return;
154
154
  }
155
155
  const method = getDestructuredPropertyName(node);
156
- if (!method || !BANNED_METHOD_NAMES.has(method)) {
156
+ if (!(method && BANNED_METHOD_NAMES.has(method))) {
157
157
  return;
158
158
  }
159
159
  // Resolve the node whose type represents the value being destructured.
@@ -31,7 +31,7 @@ function collectResultTypes(type, collection) {
31
31
  return;
32
32
  }
33
33
  const symbol = type.getSymbol();
34
- if (!symbol || !isAntithrowResultTypeSymbol(symbol)) {
34
+ if (!(symbol && isAntithrowResultTypeSymbol(symbol))) {
35
35
  collection.hasNonResultMembers = true;
36
36
  return;
37
37
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antithrow/eslint-plugin",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
4
4
  "description": "ESLint plugin for antithrow Result types",
5
5
  "license": "MIT",
6
6
  "author": "Jack Weilage",
@@ -9,13 +9,20 @@
9
9
  "url": "git+https://github.com/antithrow/antithrow.git",
10
10
  "directory": "packages/eslint-plugin"
11
11
  },
12
+ "homepage": "https://antithrow.dev",
13
+ "bugs": {
14
+ "url": "https://github.com/antithrow/antithrow/issues"
15
+ },
12
16
  "keywords": [
17
+ "antithrow",
13
18
  "eslint",
14
19
  "eslintplugin",
20
+ "eslint-plugin",
15
21
  "result",
16
22
  "error-handling",
17
23
  "typescript"
18
24
  ],
25
+ "sideEffects": false,
19
26
  "type": "module",
20
27
  "exports": {
21
28
  ".": {
@@ -41,7 +48,7 @@
41
48
  "devDependencies": {
42
49
  "@typescript-eslint/parser": "8.54.0",
43
50
  "@typescript-eslint/rule-tester": "8.54.0",
44
- "antithrow": "workspace:*",
51
+ "antithrow": "workspace:^",
45
52
  "eslint": "9.39.2",
46
53
  "typescript": "5.9.3"
47
54
  },