@onrails/eslint-plugin 0.1.0 → 0.1.2

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.
Files changed (2) hide show
  1. package/package.json +3 -3
  2. package/src/index.js +62 -9
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onrails/eslint-plugin",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "ESLint rules for @onrails/result boundaries — flags Promise<Result<…>> in public APIs and _unsafeUnwrap*. Replaces unmaintained eslint-plugin-neverthrow.",
5
5
  "license": "MIT",
6
6
  "author": "Alan R. Soares",
@@ -49,9 +49,9 @@
49
49
  }
50
50
  },
51
51
  "scripts": {
52
- "typecheck": "echo 'noop (js plugin)'",
52
+ "typecheck": "tsc --noEmit",
53
53
  "test": "bun test",
54
- "check": "bun test"
54
+ "check": "bun run typecheck && bun test"
55
55
  },
56
56
  "devDependencies": {
57
57
  "@typescript-eslint/parser": "^8.60.0",
package/src/index.js CHANGED
@@ -6,9 +6,48 @@
6
6
  * for TypeScript files; without it the rules never fire.
7
7
  */
8
8
 
9
+ /**
10
+ * Loose AST-node shape — typescript-eslint nodes (TSTypeReference, …) are not
11
+ * part of ESLint's bundled estree types, so rules narrow on `type` manually.
12
+ *
13
+ * @param {{ type: string; name?: string } | null | undefined} node
14
+ * @param {string} name
15
+ * @returns {boolean}
16
+ */
9
17
  const isIdentifierNamed = (node, name) =>
10
- node && node.type === "Identifier" && node.name === name;
18
+ Boolean(node && node.type === "Identifier" && node.name === name);
19
+
20
+ /**
21
+ * Minimal structural view of `@typescript-eslint/parser`'s TSTypeReference —
22
+ * only the fields these rules read. ESLint's bundled types know estree alone,
23
+ * so TS-AST visitors type their own slice and bridge at report boundaries.
24
+ *
25
+ * @typedef {object} TsTypeReference
26
+ * @property {string} type
27
+ * @property {{ type: string; name?: string }} typeName
28
+ * @property {{ params: TsTypeReference[] } | undefined} [typeArguments]
29
+ */
30
+
31
+ /**
32
+ * Safe: TS-AST nodes carry the same range/loc contract ESLint needs; the
33
+ * bundled estree types just cannot name them.
34
+ *
35
+ * @param {TsTypeReference} node
36
+ * @returns {import('eslint').Rule.Node}
37
+ */
38
+ const asReportable = (node) =>
39
+ /** @type {import('eslint').Rule.Node} */ (/** @type {unknown} */ (node));
11
40
 
41
+ /**
42
+ * Safe: the `TSTypeReference` visitor key guarantees the parser produced a
43
+ * typescript-eslint TSTypeReference node.
44
+ *
45
+ * @param {import('eslint').Rule.Node} node
46
+ * @returns {TsTypeReference}
47
+ */
48
+ const asTsTypeReference = (node) => /** @type {TsTypeReference} */ (/** @type {unknown} */ (node));
49
+
50
+ /** @type {import('eslint').Rule.RuleModule} */
12
51
  const noPromiseResult = {
13
52
  meta: {
14
53
  type: "problem",
@@ -25,18 +64,19 @@ const noPromiseResult = {
25
64
  create(context) {
26
65
  const sourceCode = context.sourceCode;
27
66
  return {
28
- TSTypeReference(node) {
29
- if (!isIdentifierNamed(node.typeName, "Promise")) return;
30
- const args = node.typeArguments?.params;
67
+ TSTypeReference(/** @type {import('eslint').Rule.Node} */ node) {
68
+ const typeRef = asTsTypeReference(node);
69
+ if (!isIdentifierNamed(typeRef.typeName, "Promise")) return;
70
+ const args = typeRef.typeArguments?.params;
31
71
  const inner = args?.[0];
32
- if (!inner || inner.type !== "TSTypeReference") return;
72
+ if (inner?.type !== "TSTypeReference") return;
33
73
  if (!isIdentifierNamed(inner.typeName, "Result")) return;
34
74
 
35
75
  const innerArgs = inner.typeArguments?.params;
36
76
  const okT = innerArgs?.[0];
37
77
  const errT = innerArgs?.[1];
38
- const okText = okT ? sourceCode.getText(okT) : "T";
39
- const errText = errT ? sourceCode.getText(errT) : "E";
78
+ const okText = okT ? sourceCode.getText(asReportable(okT)) : "T";
79
+ const errText = errT ? sourceCode.getText(asReportable(errT)) : "E";
40
80
 
41
81
  context.report({
42
82
  node,
@@ -49,15 +89,18 @@ const noPromiseResult = {
49
89
  },
50
90
  };
51
91
 
92
+ /** @type {import('eslint').Rule.RuleModule} */
52
93
  const noUnsafeUnwrap = {
53
94
  meta: {
54
95
  type: "suggestion",
55
96
  docs: {
56
- description: "Discourage _unsafeUnwrap / _unsafeUnwrapErr outside tests.",
97
+ description:
98
+ "Discourage _unsafeUnwrap / _unsafeUnwrapErr and unwrapOk / unwrapErr / unwrap outside tests. Matching is name-based; same-named functions from other libraries are flagged too.",
57
99
  },
58
100
  schema: [],
59
101
  messages: {
60
- unsafe: "Avoid _unsafeUnwrap* — use match(), resolve(), or yieldResult() in tryGen.",
102
+ unsafe:
103
+ "Avoid _unsafeUnwrap* and unwrap* outside tests — use match(), resolve(), or yieldResult() in tryGen.",
61
104
  },
62
105
  },
63
106
  create(context) {
@@ -74,6 +117,16 @@ const noUnsafeUnwrap = {
74
117
  context.report({ node, messageId: "unsafe" });
75
118
  }
76
119
  },
120
+ CallExpression(node) {
121
+ if (
122
+ node.callee.type === "Identifier" &&
123
+ (node.callee.name === "unwrapOk" ||
124
+ node.callee.name === "unwrapErr" ||
125
+ node.callee.name === "unwrap")
126
+ ) {
127
+ context.report({ node, messageId: "unsafe" });
128
+ }
129
+ },
77
130
  };
78
131
  },
79
132
  };