@dangayle/eslint-plugin-rustlike 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/dist/index.cjs +271 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +18 -0
- package/dist/index.d.cts.map +1 -0
- package/package.json +56 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
Object.defineProperties(exports, { __esModule: { value: true }, [Symbol.toStringTag]: { value: 'Module' } });
|
|
2
|
+
|
|
3
|
+
//#region src/configs/recommended.ts
|
|
4
|
+
/**
|
|
5
|
+
* Recommended rule set for eslint-plugin-rustlike.
|
|
6
|
+
* Exported separately so the plugin can build flat configs with the plugin object.
|
|
7
|
+
*/
|
|
8
|
+
const recommendedRules = {
|
|
9
|
+
"rustlike/no-object-spread-on-adt": "warn",
|
|
10
|
+
"rustlike/prefer-match": "warn"
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
//#endregion
|
|
14
|
+
//#region src/configs/strict.ts
|
|
15
|
+
/**
|
|
16
|
+
* Strict rule set for eslint-plugin-rustlike (opt-in).
|
|
17
|
+
* Extends recommended rules with stronger enforcement.
|
|
18
|
+
*/
|
|
19
|
+
const strictRules = {
|
|
20
|
+
...recommendedRules,
|
|
21
|
+
"rustlike/no-unwrap": "error",
|
|
22
|
+
"rustlike/no-throw-in-result-returning-function": "error"
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
//#endregion
|
|
26
|
+
//#region src/rules/no-object-spread-on-adt.ts
|
|
27
|
+
const adtNames = new Set([
|
|
28
|
+
"Ok",
|
|
29
|
+
"Err",
|
|
30
|
+
"Some",
|
|
31
|
+
"None",
|
|
32
|
+
"Result",
|
|
33
|
+
"Option"
|
|
34
|
+
]);
|
|
35
|
+
function isAdtCall(node) {
|
|
36
|
+
if (node.type === "CallExpression") {
|
|
37
|
+
const callee = node.callee;
|
|
38
|
+
if (callee.type === "Identifier" && adtNames.has(callee.name)) return true;
|
|
39
|
+
if (callee.type === "MemberExpression" && callee.object.type === "Identifier" && adtNames.has(callee.object.name)) return true;
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
function getAdtName(node) {
|
|
44
|
+
if (node.type === "CallExpression") {
|
|
45
|
+
const callee = node.callee;
|
|
46
|
+
if (callee.type === "Identifier") return callee.name;
|
|
47
|
+
if (callee.type === "MemberExpression" && callee.object.type === "Identifier") return `${callee.object.name}.${callee.property.type === "Identifier" ? callee.property.name : "?"}`;
|
|
48
|
+
}
|
|
49
|
+
return "ADT";
|
|
50
|
+
}
|
|
51
|
+
const rule$3 = {
|
|
52
|
+
meta: {
|
|
53
|
+
type: "problem",
|
|
54
|
+
docs: {
|
|
55
|
+
description: "Disallow spreading or Object.assign on Result/Option values",
|
|
56
|
+
url: "https://github.com/dangayle/rustlike/blob/main/docs/eslint-rules/no-object-spread-on-adt.md"
|
|
57
|
+
},
|
|
58
|
+
messages: {
|
|
59
|
+
noSpreadOnAdt: "Spreading Result/Option values ({{ name }}) strips methods. Use .match() or other combinators instead.",
|
|
60
|
+
noAssignOnAdt: "Object.assign on Result/Option values ({{ name }}) strips methods. Use .match() or other combinators instead."
|
|
61
|
+
},
|
|
62
|
+
schema: []
|
|
63
|
+
},
|
|
64
|
+
create(context) {
|
|
65
|
+
return {
|
|
66
|
+
SpreadElement(node) {
|
|
67
|
+
const argument = node.argument;
|
|
68
|
+
if (isAdtCall(argument)) context.report({
|
|
69
|
+
node,
|
|
70
|
+
messageId: "noSpreadOnAdt",
|
|
71
|
+
data: { name: getAdtName(argument) }
|
|
72
|
+
});
|
|
73
|
+
},
|
|
74
|
+
CallExpression(node) {
|
|
75
|
+
const callee = node.callee;
|
|
76
|
+
if (callee.type === "MemberExpression" && callee.object.type === "Identifier" && callee.object.name === "Object" && callee.property.type === "Identifier" && callee.property.name === "assign") for (let i = 1; i < node.arguments.length; i++) {
|
|
77
|
+
const arg = node.arguments[i];
|
|
78
|
+
if (arg && isAdtCall(arg)) context.report({
|
|
79
|
+
node: arg,
|
|
80
|
+
messageId: "noAssignOnAdt",
|
|
81
|
+
data: { name: getAdtName(arg) }
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
//#endregion
|
|
90
|
+
//#region src/rules/prefer-match.ts
|
|
91
|
+
const typeGuardMethods = new Set([
|
|
92
|
+
"isOk",
|
|
93
|
+
"isErr",
|
|
94
|
+
"isSome",
|
|
95
|
+
"isNone"
|
|
96
|
+
]);
|
|
97
|
+
const rule$2 = {
|
|
98
|
+
meta: {
|
|
99
|
+
type: "suggestion",
|
|
100
|
+
docs: {
|
|
101
|
+
description: "Suggest using .match() for simple if/else on Result/Option",
|
|
102
|
+
url: "https://github.com/dangayle/rustlike/blob/main/docs/eslint-rules/prefer-match.md"
|
|
103
|
+
},
|
|
104
|
+
messages: { preferMatch: "Consider using .match() instead of if/else for cleaner Result/Option handling." },
|
|
105
|
+
schema: []
|
|
106
|
+
},
|
|
107
|
+
create(context) {
|
|
108
|
+
function isTypeGuardCall(node) {
|
|
109
|
+
if (node.type === "CallExpression" && node.callee.type === "MemberExpression" && node.callee.property.type === "Identifier" && typeGuardMethods.has(node.callee.property.name)) return true;
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
function isSingleReturnStatement(node) {
|
|
113
|
+
if (node.type === "ReturnStatement") return true;
|
|
114
|
+
if (node.type === "BlockStatement" && node.body.length === 1) return node.body[0]?.type === "ReturnStatement";
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
IfStatement(node) {
|
|
119
|
+
if (!isTypeGuardCall(node.test)) return;
|
|
120
|
+
if (!node.alternate) return;
|
|
121
|
+
if (node.alternate.type === "IfStatement") return;
|
|
122
|
+
if (!isSingleReturnStatement(node.consequent)) return;
|
|
123
|
+
if (!isSingleReturnStatement(node.alternate)) return;
|
|
124
|
+
context.report({
|
|
125
|
+
node,
|
|
126
|
+
messageId: "preferMatch"
|
|
127
|
+
});
|
|
128
|
+
},
|
|
129
|
+
ConditionalExpression(node) {
|
|
130
|
+
if (!isTypeGuardCall(node.test)) return;
|
|
131
|
+
const parent = node.parent;
|
|
132
|
+
if (parent?.type === "ReturnStatement" || parent?.type === "VariableDeclarator" || parent?.type === "AssignmentExpression") context.report({
|
|
133
|
+
node,
|
|
134
|
+
messageId: "preferMatch"
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
//#endregion
|
|
142
|
+
//#region src/rules/no-unwrap.ts
|
|
143
|
+
const unwrapMethods = new Set([
|
|
144
|
+
"unwrap",
|
|
145
|
+
"unwrapErr",
|
|
146
|
+
"expect"
|
|
147
|
+
]);
|
|
148
|
+
const rule$1 = {
|
|
149
|
+
meta: {
|
|
150
|
+
type: "problem",
|
|
151
|
+
docs: {
|
|
152
|
+
description: "Disallow .unwrap(), .unwrapErr(), and .expect() calls",
|
|
153
|
+
url: "https://github.com/dangayle/rustlike/blob/main/docs/eslint-rules/no-unwrap.md"
|
|
154
|
+
},
|
|
155
|
+
messages: {
|
|
156
|
+
noUnwrap: "Avoid .unwrap() - it can panic. Use .match(), .unwrapOr(), or .unwrapOrElse() instead.",
|
|
157
|
+
noUnwrapErr: "Avoid .unwrapErr() - it can panic. Use .match() to handle both cases explicitly.",
|
|
158
|
+
noExpect: "Avoid .expect() - it can panic. Use .match(), .unwrapOr(), or document why the invariant holds."
|
|
159
|
+
},
|
|
160
|
+
schema: []
|
|
161
|
+
},
|
|
162
|
+
create(context) {
|
|
163
|
+
return { CallExpression(node) {
|
|
164
|
+
const callee = node.callee;
|
|
165
|
+
if (callee.type === "MemberExpression" && callee.property.type === "Identifier" && unwrapMethods.has(callee.property.name)) {
|
|
166
|
+
const methodName = callee.property.name;
|
|
167
|
+
let messageId;
|
|
168
|
+
switch (methodName) {
|
|
169
|
+
case "unwrap":
|
|
170
|
+
messageId = "noUnwrap";
|
|
171
|
+
break;
|
|
172
|
+
case "unwrapErr":
|
|
173
|
+
messageId = "noUnwrapErr";
|
|
174
|
+
break;
|
|
175
|
+
case "expect":
|
|
176
|
+
messageId = "noExpect";
|
|
177
|
+
break;
|
|
178
|
+
default: return;
|
|
179
|
+
}
|
|
180
|
+
context.report({
|
|
181
|
+
node,
|
|
182
|
+
messageId
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
} };
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
//#endregion
|
|
190
|
+
//#region src/rules/no-throw-in-result-returning-function.ts
|
|
191
|
+
const rule = {
|
|
192
|
+
meta: {
|
|
193
|
+
type: "problem",
|
|
194
|
+
docs: {
|
|
195
|
+
description: "Disallow throw statements in functions returning Result",
|
|
196
|
+
url: "https://github.com/dangayle/rustlike/blob/main/docs/eslint-rules/no-throw-in-result-returning-function.md"
|
|
197
|
+
},
|
|
198
|
+
messages: { noThrowInResult: "Do not throw in a function returning Result. Use Err() to return error values instead." },
|
|
199
|
+
schema: []
|
|
200
|
+
},
|
|
201
|
+
create(context) {
|
|
202
|
+
const resultFunctionStack = [];
|
|
203
|
+
function hasResultReturnType(node) {
|
|
204
|
+
const castNode = node;
|
|
205
|
+
let returnType;
|
|
206
|
+
if (node.type === "FunctionDeclaration" || node.type === "FunctionExpression") returnType = castNode.returnType;
|
|
207
|
+
else if (node.type === "ArrowFunctionExpression") returnType = castNode.returnType;
|
|
208
|
+
if (!returnType) return false;
|
|
209
|
+
const typeText = context.sourceCode.getText(returnType);
|
|
210
|
+
return typeText.includes("Result<") || typeText.includes("Result ");
|
|
211
|
+
}
|
|
212
|
+
function enterFunction(node) {
|
|
213
|
+
if (hasResultReturnType(node)) resultFunctionStack.push(true);
|
|
214
|
+
else if ((node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression") && node.returnType !== void 0) resultFunctionStack.push(false);
|
|
215
|
+
else {
|
|
216
|
+
const parentIsResult = resultFunctionStack[resultFunctionStack.length - 1] === true;
|
|
217
|
+
resultFunctionStack.push(parentIsResult);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
function exitFunction() {
|
|
221
|
+
resultFunctionStack.pop();
|
|
222
|
+
}
|
|
223
|
+
function isInsideResultFunction() {
|
|
224
|
+
return resultFunctionStack.length > 0 && resultFunctionStack[resultFunctionStack.length - 1] === true;
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
FunctionDeclaration: enterFunction,
|
|
228
|
+
"FunctionDeclaration:exit": exitFunction,
|
|
229
|
+
FunctionExpression: enterFunction,
|
|
230
|
+
"FunctionExpression:exit": exitFunction,
|
|
231
|
+
ArrowFunctionExpression: enterFunction,
|
|
232
|
+
"ArrowFunctionExpression:exit": exitFunction,
|
|
233
|
+
ThrowStatement(node) {
|
|
234
|
+
if (isInsideResultFunction()) context.report({
|
|
235
|
+
node,
|
|
236
|
+
messageId: "noThrowInResult"
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
//#endregion
|
|
244
|
+
//#region src/index.ts
|
|
245
|
+
const rules = {
|
|
246
|
+
"no-object-spread-on-adt": rule$3,
|
|
247
|
+
"prefer-match": rule$2,
|
|
248
|
+
"no-unwrap": rule$1,
|
|
249
|
+
"no-throw-in-result-returning-function": rule
|
|
250
|
+
};
|
|
251
|
+
const plugin = { rules };
|
|
252
|
+
const configs = {
|
|
253
|
+
recommended: {
|
|
254
|
+
plugins: { rustlike: plugin },
|
|
255
|
+
rules: recommendedRules
|
|
256
|
+
},
|
|
257
|
+
strict: {
|
|
258
|
+
plugins: { rustlike: plugin },
|
|
259
|
+
rules: strictRules
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
const exportedDefault = {
|
|
263
|
+
rules: plugin.rules,
|
|
264
|
+
configs
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
//#endregion
|
|
268
|
+
exports.configs = configs;
|
|
269
|
+
exports.default = exportedDefault;
|
|
270
|
+
exports.rules = rules;
|
|
271
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["rule","rule","rule","noObjectSpreadOnAdt","preferMatch","noUnwrap","noThrowInResultReturningFunction"],"sources":["../src/configs/recommended.ts","../src/configs/strict.ts","../src/rules/no-object-spread-on-adt.ts","../src/rules/prefer-match.ts","../src/rules/no-unwrap.ts","../src/rules/no-throw-in-result-returning-function.ts","../src/index.ts"],"sourcesContent":["import type { Linter } from \"eslint\";\n\n/**\n * Recommended rule set for eslint-plugin-rustlike.\n * Exported separately so the plugin can build flat configs with the plugin object.\n */\nexport const recommendedRules: Linter.RulesRecord = {\n \"rustlike/no-object-spread-on-adt\": \"warn\",\n \"rustlike/prefer-match\": \"warn\",\n};\n\n// Legacy-style config (kept for compatibility with non-flat consumers).\nconst config: Linter.LegacyConfig = {\n plugins: [\"rustlike\"],\n rules: recommendedRules,\n};\n\nexport default config;\n","import type { Linter } from \"eslint\";\nimport { recommendedRules } from \"./recommended\";\n\n/**\n * Strict rule set for eslint-plugin-rustlike (opt-in).\n * Extends recommended rules with stronger enforcement.\n */\nexport const strictRules = {\n ...recommendedRules,\n \"rustlike/no-unwrap\": \"error\",\n \"rustlike/no-throw-in-result-returning-function\": \"error\",\n} satisfies Linter.RulesRecord;\n\n// Legacy-style config (kept for compatibility with non-flat consumers).\nconst config: Linter.LegacyConfig = {\n plugins: [\"rustlike\"],\n rules: strictRules,\n};\n\nexport default config;\n","/**\n * Rule: no-object-spread-on-adt\n *\n * Warns when spreading or Object.assign is used on Result/Option ADT values,\n * which would strip the methods from the objects.\n */\nimport type { Rule } from \"eslint\";\n\n// ADT constructor/factory names to detect\nconst adtNames = new Set([\"Ok\", \"Err\", \"Some\", \"None\", \"Result\", \"Option\"]);\n\nfunction isAdtCall(node: Rule.Node): boolean {\n if (node.type === \"CallExpression\") {\n const callee = node.callee;\n if (callee.type === \"Identifier\" && adtNames.has(callee.name)) {\n return true;\n }\n // Check for Result.ok(), Option.some(), etc.\n if (\n callee.type === \"MemberExpression\" &&\n callee.object.type === \"Identifier\" &&\n adtNames.has(callee.object.name)\n ) {\n return true;\n }\n }\n return false;\n}\n\nfunction getAdtName(node: Rule.Node): string {\n if (node.type === \"CallExpression\") {\n const callee = node.callee;\n if (callee.type === \"Identifier\") {\n return callee.name;\n }\n if (callee.type === \"MemberExpression\" && callee.object.type === \"Identifier\") {\n return `${callee.object.name}.${callee.property.type === \"Identifier\" ? callee.property.name : \"?\"}`;\n }\n }\n return \"ADT\";\n}\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: \"problem\",\n docs: {\n description: \"Disallow spreading or Object.assign on Result/Option values\",\n url: \"https://github.com/dangayle/rustlike/blob/main/docs/eslint-rules/no-object-spread-on-adt.md\",\n },\n messages: {\n noSpreadOnAdt:\n \"Spreading Result/Option values ({{ name }}) strips methods. Use .match() or other combinators instead.\",\n noAssignOnAdt:\n \"Object.assign on Result/Option values ({{ name }}) strips methods. Use .match() or other combinators instead.\",\n },\n schema: [],\n },\n create(context) {\n return {\n // Detect { ...Ok(x) } or { ...someResultVar }\n SpreadElement(node) {\n const argument = node.argument;\n if (isAdtCall(argument as Rule.Node)) {\n context.report({\n node,\n messageId: \"noSpreadOnAdt\",\n data: { name: getAdtName(argument as Rule.Node) },\n });\n }\n },\n\n // Detect Object.assign({}, Ok(x))\n CallExpression(node) {\n const callee = node.callee;\n if (\n callee.type === \"MemberExpression\" &&\n callee.object.type === \"Identifier\" &&\n callee.object.name === \"Object\" &&\n callee.property.type === \"Identifier\" &&\n callee.property.name === \"assign\"\n ) {\n // Check if any argument (except first) is an ADT call\n for (let i = 1; i < node.arguments.length; i++) {\n const arg = node.arguments[i];\n if (arg && isAdtCall(arg as Rule.Node)) {\n context.report({\n node: arg,\n messageId: \"noAssignOnAdt\",\n data: { name: getAdtName(arg as Rule.Node) },\n });\n }\n }\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: prefer-match\n *\n * Suggests using .match() instead of if/else or ternary when both branches\n * handle Result/Option in a straightforward way.\n *\n * Only triggers on high-signal patterns:\n * - if (x.isOk()) return A; else return B;\n * - return x.isOk() ? A : B;\n * - Same for Option (isSome/isNone)\n */\nimport type { Rule } from \"eslint\";\n\n// Method names that indicate Result/Option type guards\nconst typeGuardMethods = new Set([\"isOk\", \"isErr\", \"isSome\", \"isNone\"]);\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: \"suggestion\",\n docs: {\n description: \"Suggest using .match() for simple if/else on Result/Option\",\n url: \"https://github.com/dangayle/rustlike/blob/main/docs/eslint-rules/prefer-match.md\",\n },\n messages: {\n preferMatch: \"Consider using .match() instead of if/else for cleaner Result/Option handling.\",\n },\n schema: [],\n },\n create(context) {\n function isTypeGuardCall(node: Rule.Node): boolean {\n if (\n node.type === \"CallExpression\" &&\n node.callee.type === \"MemberExpression\" &&\n node.callee.property.type === \"Identifier\" &&\n typeGuardMethods.has(node.callee.property.name)\n ) {\n return true;\n }\n return false;\n }\n\n function isSingleReturnStatement(node: Rule.Node): boolean {\n if (node.type === \"ReturnStatement\") {\n return true;\n }\n if (node.type === \"BlockStatement\" && node.body.length === 1) {\n return node.body[0]?.type === \"ReturnStatement\";\n }\n return false;\n }\n\n return {\n // Check if statements: if (x.isOk()) return A; else return B;\n IfStatement(node) {\n // Only trigger if:\n // 1. Test is a type guard call (isOk, isErr, isSome, isNone)\n // 2. Both consequent and alternate are single return statements\n // 3. There's no else-if chain\n if (!isTypeGuardCall(node.test as Rule.Node)) {\n return;\n }\n\n if (!node.alternate) {\n return; // No else branch\n }\n\n if (node.alternate.type === \"IfStatement\") {\n return; // else-if chain, too complex\n }\n\n if (!isSingleReturnStatement(node.consequent as Rule.Node)) {\n return;\n }\n\n if (!isSingleReturnStatement(node.alternate as Rule.Node)) {\n return;\n }\n\n context.report({\n node,\n messageId: \"preferMatch\",\n });\n },\n\n // Check ternaries: x.isOk() ? A : B\n ConditionalExpression(node) {\n if (!isTypeGuardCall(node.test as Rule.Node)) {\n return;\n }\n\n // Only suggest if this is a return or assignment context\n // (not deeply nested in other expressions)\n const parent = (node as Rule.Node).parent;\n if (\n parent?.type === \"ReturnStatement\" ||\n parent?.type === \"VariableDeclarator\" ||\n parent?.type === \"AssignmentExpression\"\n ) {\n context.report({\n node,\n messageId: \"preferMatch\",\n });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: no-unwrap\n *\n * Bans .unwrap(), .unwrapErr(), and .expect() calls to enforce\n * explicit error handling via .match(), .unwrapOr(), etc.\n */\nimport type { Rule } from \"eslint\";\n\n// Methods that can panic\nconst unwrapMethods = new Set([\"unwrap\", \"unwrapErr\", \"expect\"]);\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: \"problem\",\n docs: {\n description: \"Disallow .unwrap(), .unwrapErr(), and .expect() calls\",\n url: \"https://github.com/dangayle/rustlike/blob/main/docs/eslint-rules/no-unwrap.md\",\n },\n messages: {\n noUnwrap:\n \"Avoid .unwrap() - it can panic. Use .match(), .unwrapOr(), or .unwrapOrElse() instead.\",\n noUnwrapErr:\n \"Avoid .unwrapErr() - it can panic. Use .match() to handle both cases explicitly.\",\n noExpect:\n \"Avoid .expect() - it can panic. Use .match(), .unwrapOr(), or document why the invariant holds.\",\n },\n schema: [],\n },\n create(context) {\n return {\n CallExpression(node) {\n const callee = node.callee;\n\n // Check for method calls like result.unwrap()\n if (\n callee.type === \"MemberExpression\" &&\n callee.property.type === \"Identifier\" &&\n unwrapMethods.has(callee.property.name)\n ) {\n const methodName = callee.property.name;\n\n let messageId: string;\n switch (methodName) {\n case \"unwrap\":\n messageId = \"noUnwrap\";\n break;\n case \"unwrapErr\":\n messageId = \"noUnwrapErr\";\n break;\n case \"expect\":\n messageId = \"noExpect\";\n break;\n default:\n return;\n }\n\n context.report({\n node,\n messageId,\n });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * Rule: no-throw-in-result-returning-function\n *\n * Disallows `throw` statements inside functions that are annotated\n * to return Result<T, E>. This enforces the pattern of using\n * Err() for error cases instead of exceptions.\n */\nimport type { Rule } from \"eslint\";\n\nconst rule: Rule.RuleModule = {\n meta: {\n type: \"problem\",\n docs: {\n description: \"Disallow throw statements in functions returning Result\",\n url: \"https://github.com/dangayle/rustlike/blob/main/docs/eslint-rules/no-throw-in-result-returning-function.md\",\n },\n messages: {\n noThrowInResult:\n \"Do not throw in a function returning Result. Use Err() to return error values instead.\",\n },\n schema: [],\n },\n create(context) {\n // Stack to track if we're inside a Result-returning function\n const resultFunctionStack: boolean[] = [];\n\n function hasResultReturnType(node: Rule.Node): boolean {\n // Check for explicit return type annotation containing \"Result\"\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const castNode = node as any;\n let returnType: any;\n\n if (node.type === \"FunctionDeclaration\" || node.type === \"FunctionExpression\") {\n returnType = castNode.returnType;\n } else if (node.type === \"ArrowFunctionExpression\") {\n returnType = castNode.returnType;\n }\n\n if (!returnType) {\n return false;\n }\n\n // Get the source code of the return type annotation\n const typeText = context.sourceCode.getText(returnType);\n\n // Check if it contains \"Result<\" (simple heuristic)\n return typeText.includes(\"Result<\") || typeText.includes(\"Result \");\n }\n\n function enterFunction(node: Rule.Node): void {\n if (hasResultReturnType(node)) {\n resultFunctionStack.push(true);\n } else if (\n (node.type === \"FunctionDeclaration\" ||\n node.type === \"FunctionExpression\" ||\n node.type === \"ArrowFunctionExpression\") &&\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (node as any).returnType !== undefined\n ) {\n // Has explicit non-Result return type — don't inherit parent context\n resultFunctionStack.push(false);\n } else {\n // No return type annotation — inherit parent context (e.g. callbacks)\n const parentIsResult = resultFunctionStack[resultFunctionStack.length - 1] === true;\n resultFunctionStack.push(parentIsResult);\n }\n }\n\n function exitFunction(): void {\n resultFunctionStack.pop();\n }\n\n function isInsideResultFunction(): boolean {\n return (\n resultFunctionStack.length > 0 &&\n resultFunctionStack[resultFunctionStack.length - 1] === true\n );\n }\n\n return {\n FunctionDeclaration: enterFunction,\n \"FunctionDeclaration:exit\": exitFunction,\n FunctionExpression: enterFunction,\n \"FunctionExpression:exit\": exitFunction,\n ArrowFunctionExpression: enterFunction,\n \"ArrowFunctionExpression:exit\": exitFunction,\n\n ThrowStatement(node) {\n if (isInsideResultFunction()) {\n context.report({\n node,\n messageId: \"noThrowInResult\",\n });\n }\n },\n };\n },\n};\n\nexport default rule;\n","/**\n * eslint-plugin-rustlike\n *\n * ESLint rules to enforce Rust-like patterns when using the rustlike library.\n * Provides 'recommended' and 'strict' presets.\n */\n\nimport type { ESLint, Linter, Rule } from \"eslint\";\nimport { recommendedRules } from \"./configs/recommended\";\nimport { strictRules } from \"./configs/strict\";\n\nimport noObjectSpreadOnAdt from \"./rules/no-object-spread-on-adt\";\nimport preferMatch from \"./rules/prefer-match\";\nimport noUnwrap from \"./rules/no-unwrap\";\nimport noThrowInResultReturningFunction from \"./rules/no-throw-in-result-returning-function\";\n\nconst rules: Record<string, Rule.RuleModule> = {\n \"no-object-spread-on-adt\": noObjectSpreadOnAdt,\n \"prefer-match\": preferMatch,\n \"no-unwrap\": noUnwrap,\n \"no-throw-in-result-returning-function\": noThrowInResultReturningFunction,\n};\n\nconst plugin: ESLint.Plugin = { rules };\n\nconst recommended: Linter.FlatConfig = {\n plugins: { rustlike: plugin },\n rules: recommendedRules,\n};\n\nconst strict: Linter.FlatConfig = {\n plugins: { rustlike: plugin },\n rules: strictRules,\n};\n\nconst configs: { recommended: Linter.FlatConfig; strict: Linter.FlatConfig } = {\n recommended,\n strict,\n};\n\n// Default export for ESLint plugin\nexport { rules, configs };\nconst exportedDefault: {\n rules: ESLint.Plugin[\"rules\"];\n configs: { recommended: Linter.FlatConfig; strict: Linter.FlatConfig };\n} = { rules: plugin.rules, configs };\nexport default exportedDefault;\n"],"mappings":"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"}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ESLint, Linter, Rule } from "eslint";
|
|
2
|
+
|
|
3
|
+
//#region src/index.d.ts
|
|
4
|
+
declare const rules: Record<string, Rule.RuleModule>;
|
|
5
|
+
declare const configs: {
|
|
6
|
+
recommended: Linter.FlatConfig;
|
|
7
|
+
strict: Linter.FlatConfig;
|
|
8
|
+
};
|
|
9
|
+
declare const exportedDefault: {
|
|
10
|
+
rules: ESLint.Plugin["rules"];
|
|
11
|
+
configs: {
|
|
12
|
+
recommended: Linter.FlatConfig;
|
|
13
|
+
strict: Linter.FlatConfig;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
//#endregion
|
|
17
|
+
export { configs, exportedDefault as default, rules };
|
|
18
|
+
//# sourceMappingURL=index.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.cts","names":[],"sources":["../src/index.ts"],"mappings":";;;cAgBM,KAAA,EAAO,MAAA,SAAe,IAAA,CAAK,UAAA;AAAA,cAmB3B,OAAA;EAAW,WAAA,EAAa,MAAA,CAAO,UAAA;EAAY,MAAA,EAAQ,MAAA,CAAO,UAAA;AAAA;AAAA,cAO1D,eAAA;EACJ,KAAA,EAAO,MAAA,CAAO,MAAA;EACd,OAAA;IAAW,WAAA,EAAa,MAAA,CAAO,UAAA;IAAY,MAAA,EAAQ,MAAA,CAAO,UAAA;EAAA;AAAA"}
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dangayle/eslint-plugin-rustlike",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"repository": {
|
|
5
|
+
"type": "git",
|
|
6
|
+
"url": "git+https://github.com/dangayle/rustlike.git",
|
|
7
|
+
"directory": "packages/eslint-plugin-rustlike"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://github.com/dangayle/rustlike/tree/main/packages/eslint-plugin-rustlike#readme",
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/dangayle/rustlike/issues"
|
|
12
|
+
},
|
|
13
|
+
"publishConfig": {
|
|
14
|
+
"access": "public"
|
|
15
|
+
},
|
|
16
|
+
"description": "ESLint rules to enforce Rust-like patterns with the rustlike library",
|
|
17
|
+
"keywords": [
|
|
18
|
+
"eslint",
|
|
19
|
+
"eslint-plugin",
|
|
20
|
+
"eslintplugin",
|
|
21
|
+
"option",
|
|
22
|
+
"pattern-matching",
|
|
23
|
+
"result",
|
|
24
|
+
"rust"
|
|
25
|
+
],
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"author": "Dan Gayle <dangayle@gmail.com>",
|
|
28
|
+
"files": [
|
|
29
|
+
"dist"
|
|
30
|
+
],
|
|
31
|
+
"main": "./dist/index.cjs",
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"exports": {
|
|
34
|
+
".": {
|
|
35
|
+
"types": "./dist/index.d.ts",
|
|
36
|
+
"require": "./dist/index.cjs"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/eslint": "^9.6.1",
|
|
41
|
+
"@types/node": "^25.3.3",
|
|
42
|
+
"@typescript-eslint/rule-tester": "^8.19.0",
|
|
43
|
+
"eslint": "^9.17.0",
|
|
44
|
+
"tsdown": "^0.20.3",
|
|
45
|
+
"typescript": "^5.3.0",
|
|
46
|
+
"vitest": "^4.0.18"
|
|
47
|
+
},
|
|
48
|
+
"peerDependencies": {
|
|
49
|
+
"eslint": ">=8.0.0"
|
|
50
|
+
},
|
|
51
|
+
"scripts": {
|
|
52
|
+
"build": "tsdown",
|
|
53
|
+
"test": "vitest run",
|
|
54
|
+
"test:watch": "vitest"
|
|
55
|
+
}
|
|
56
|
+
}
|