From 2c2691536fc279d89cba37e0bf663017aacb0512 Mon Sep 17 00:00:00 2001 From: Kenneth Udovic Date: Sun, 14 Jan 2024 12:41:33 -0500 Subject: [PATCH] chore: sjel chore: sjel chore: benchmarks --- README.md | 4 +- apps/docs/src/pages/benchmarks.mdx | 23 ++++ apps/docs/src/pages/index.mdx | 24 ++++- .../docs/src/pages/rules/ajel-const-tuples.md | 11 ++ .../src/pages/rules/ajel-disable-try-catch.md | 16 ++- .../rules/ajel-require-error-handling.md | 17 ++- .../rules/ajel-require-tuple-declaration.md | 14 +++ .../rules/ajel-strict-error-instanceof.md | 19 ++++ .../src/pages/rules/sjel-require-currying.md | 32 ++++++ packages/ajel-core/package.json | 4 +- packages/ajel-core/readme.md | 20 +++- packages/ajel-core/src/index.ts | 1 + packages/ajel-core/src/sjel.ts | 10 ++ packages/benchmarks/README.md | 11 ++ packages/benchmarks/async_tests.ts | 5 +- packages/benchmarks/benchmark.ts | 31 +++++- packages/benchmarks/sync_tests.ts | 55 ++++++++++ packages/eslint-plugin-ajel/README.md | 28 +++-- .../docs/rules/ajel-const-tuples.md | 15 ++- .../docs/rules/ajel-disable-throw.md | 4 +- .../docs/rules/ajel-disable-try-catch.md | 20 +++- .../docs/rules/ajel-require-error-handling.md | 26 ++++- .../rules/ajel-require-tuple-declaration.md | 18 +++- .../rules/ajel-strict-error-instanceof.md | 25 ++++- .../docs/rules/sjel-require-currying.md | 32 ++++++ packages/eslint-plugin-ajel/package.json | 4 +- .../src/configs/recommended.ts | 1 + packages/eslint-plugin-ajel/src/index.ts | 2 + .../src/rules/ajel-const-tuples.ts | 22 +++- .../src/rules/ajel-disable-try-catch.ts | 13 ++- .../src/rules/ajel-require-error-handling.ts | 21 +++- .../rules/ajel-require-tuple-declaration.ts | 26 +++-- .../src/rules/ajel-strict-error-instanceof.ts | 24 ++++- .../src/rules/sjel-require-currying.ts | 65 +++++++++++ .../src/utils/hasAjelCallExpressionChild.ts | 25 +++-- .../src/utils/isFoundInBinaryExpression.ts | 21 ---- .../tests/rules/ajel-const-tuples.ts | 46 +++++++- .../rules/ajel-require-error-handling.ts | 102 +++++++++++++++++- .../rules/ajel-require-tuple-declaration.ts | 23 +++- .../rules/ajel-strict-error-instanceof.ts | 29 ++++- .../tests/rules/sjel-require-currying.ts | 31 ++++++ 41 files changed, 825 insertions(+), 95 deletions(-) create mode 100644 apps/docs/src/pages/rules/sjel-require-currying.md create mode 100644 packages/ajel-core/src/sjel.ts create mode 100644 packages/benchmarks/sync_tests.ts create mode 100644 packages/eslint-plugin-ajel/docs/rules/sjel-require-currying.md create mode 100644 packages/eslint-plugin-ajel/src/rules/sjel-require-currying.ts delete mode 100644 packages/eslint-plugin-ajel/src/utils/isFoundInBinaryExpression.ts create mode 100644 packages/eslint-plugin-ajel/tests/rules/sjel-require-currying.ts diff --git a/README.md b/README.md index 7338add..a236e07 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@

Laravel Logo

-

Ajel is a 133 byte function that allows you to handle errors in a way that is similar to Golang.

+

Ajel is a 312 byte set of functions that encourage you to handle errors in a way that is similar to Golang.

NPM Version @@ -12,7 +12,7 @@ -# [ajel (Asynchronous Javascript Error Library)](https://handfish.github.io/ajel) +# [ajel](https://handfish.github.io/ajel) ### Installation diff --git a/apps/docs/src/pages/benchmarks.mdx b/apps/docs/src/pages/benchmarks.mdx index 50903e6..b6c6043 100644 --- a/apps/docs/src/pages/benchmarks.mdx +++ b/apps/docs/src/pages/benchmarks.mdx @@ -40,6 +40,17 @@ Allocate 500kb Files 2 times │ 1 │ 'ajel' │ '2,297' │ 435230.6347826253 │ '±8.77%' │ 230 │ │ 2 │ 'neverthrow' │ '2,417' │ 413718.4740590656 │ '±9.86%' │ 242 │ └─────────┴──────────────┴─────────┴───────────────────┴──────────┴─────────┘ + +------ +sjel + +Allocate 500kb Files 2 times +┌─────────┬─────────────┬─────────┬────────────────────┬──────────┬─────────┐ +│ (index) │ Task Name │ ops/sec │ Average Time (ns) │ Margin │ Samples │ +├─────────┼─────────────┼─────────┼────────────────────┼──────────┼─────────┤ +│ 0 │ 'try_catch' │ '533' │ 1873371.6187653719 │ '±6.23%' │ 54 │ +│ 1 │ 'sjel' │ '528' │ 1890484.647930793 │ '±8.25%' │ 53 │ +└─────────┴─────────────┴─────────┴────────────────────┴──────────┴─────────┘ ``` @@ -73,3 +84,15 @@ Allocate 500kb Files 2 times │ 1 │ 'ajel' │ '2,408' │ 415190.9061981929 │ '±6.26%' │ 241 │ │ 2 │ 'neverthrow' │ '2,367' │ 422311.19659882557 │ '±8.50%' │ 237 │ └─────────┴──────────────┴─────────┴────────────────────┴──────────┴─────────┘ + +------ +sjel + +Allocate 500kb Files 2 times +┌─────────┬─────────────┬─────────┬───────────────────┬──────────┬─────────┐ +│ (index) │ Task Name │ ops/sec │ Average Time (ns) │ Margin │ Samples │ +├─────────┼─────────────┼─────────┼───────────────────┼──────────┼─────────┤ +│ 0 │ 'try_catch' │ '594' │ 1682249.085108439 │ '±5.05%' │ 60 │ +│ 1 │ 'sjel' │ '595' │ 1679521.369934082 │ '±3.10%' │ 60 │ +└─────────┴─────────────┴─────────┴───────────────────┴──────────┴─────────┘ +``` diff --git a/apps/docs/src/pages/index.mdx b/apps/docs/src/pages/index.mdx index 8df0ac3..cac7770 100644 --- a/apps/docs/src/pages/index.mdx +++ b/apps/docs/src/pages/index.mdx @@ -1,8 +1,8 @@ -# ajel (Asynchronous JavaScript Error Library) +# ajel ## What is ajel? -Ajel is a **133 byte** library that allows you to handle errors in a way that is similar to Golang. +Ajel is a **312 byte** set of functions that encourage you to handle errors in a way that is similar to Golang. ### Installation @@ -14,6 +14,7 @@ Ajel is a **133 byte** library that allows you to handle errors in a way that is ### Usage ```typescript +// Handling async functions that throw import { ajel } from 'ajel'; async function main() { @@ -27,10 +28,25 @@ async function main() { } ``` -`ajel` is a function that consumes a promise and returns a tuple representing the result and the error. +```typescript +// Handling synchronous functions that throw +import { sjel } from 'ajel'; + +function main() { + const [result, err] = sjel(JSON.parse)("{}"); + + if (err) { + return err; + } + + return result; +} +``` + +`ajel` and `sjel` are a set of functions that consume a promise and returns a tuple representing the result and the error. On success, the result item has value. On error, the error item has value. It's that simple. -More interestingly, it comes with a series of linting tools to help enforce the paradigm available in the package `eslint-plugin-ajel` +More interestingly, they come with a series of linting tools to help enforce the paradigm available in the package `eslint-plugin-ajel` ## Why I ported Golang's most hated feature to JavaScript diff --git a/apps/docs/src/pages/rules/ajel-const-tuples.md b/apps/docs/src/pages/rules/ajel-const-tuples.md index 90cbf4a..488c0e8 100644 --- a/apps/docs/src/pages/rules/ajel-const-tuples.md +++ b/apps/docs/src/pages/rules/ajel-const-tuples.md @@ -15,6 +15,7 @@ This rule checks for Variable Declarations that include a call to the ajel metho ## Options - `ajelAlias` (default: 'ajel'): Specify the alias for the ajel method. This allows you to customize the method name if it differs from the default 'ajel'. +- `sjelAlias` (default: 'sjel'): Specify the alias for the sjel method. This allows you to customize the method name if it differs from the default 'sjel'. ## Implementation @@ -24,9 +25,19 @@ This rule checks for Variable Declarations that include a call to the ajel metho ## Examples ```javascript +// ajel // Bad: Variable declaration without 'const' for ajel method let [res, err] = await ajel(Promise.resolve(1)); // Good: Variable declaration with 'const' for ajel method const [res, err] = await ajel(Promise.resolve(1)); + +// ----- + +// sjel +// Bad: Variable declaration without 'const' for sjel method +let [res, err] = sjel(() => 1)(); + +// Good: Variable declaration with 'const' for sjel method +const [res, err] = sjel(() => 1)(); ``` diff --git a/apps/docs/src/pages/rules/ajel-disable-try-catch.md b/apps/docs/src/pages/rules/ajel-disable-try-catch.md index 7d9413f..18c4976 100644 --- a/apps/docs/src/pages/rules/ajel-disable-try-catch.md +++ b/apps/docs/src/pages/rules/ajel-disable-try-catch.md @@ -19,6 +19,7 @@ The use of try-catch blocks does not align with preferred error-handling strateg ## Options - `ajelAlias` (default: 'ajel'): Specify the alias for the ajel method. This allows you to customize the method name if it differs from the default 'ajel'. +- `sjelAlias` (default: 'sjel'): Specify the alias for the sjel method. This allows you to customize the method name if it differs from the default 'sjel'. ## Implementation @@ -28,6 +29,7 @@ The use of try-catch blocks does not align with preferred error-handling strateg ## Examples ```javascript +// ajel // Bad: Using TryStatement for error handling let res; @@ -41,5 +43,17 @@ try { // Better: Utilizing ajel method for error handling const [res, err] = await ajel(dangerousOperation()); -if (err) return err; +// ----- + +// sjel +// Bad: Using TryStatement for error handling +try { + res = foo; + JSON.parse("{'badjson`: 'asd'}"); +} catch (error) { + return error; +} + +// Better: Utilizing sjel method for error handling +const [res, err] = sjel(JSON.parse)("{'badjson`: 'asd'}"); ``` diff --git a/apps/docs/src/pages/rules/ajel-require-error-handling.md b/apps/docs/src/pages/rules/ajel-require-error-handling.md index 99f5e28..b851cec 100644 --- a/apps/docs/src/pages/rules/ajel-require-error-handling.md +++ b/apps/docs/src/pages/rules/ajel-require-error-handling.md @@ -8,7 +8,6 @@ The `ajel-require-error-handling` rule ensures that when calling `ajel`, the error variable (typically named `err`) is properly handled. This helps prevent potential bugs and ensures that developers explicitly address errors returned from `ajel` calls. - ## Rule Details This rule checks for the usage of the error variable (`err`) in the context of a `VariableDeclaration` associated with an `ajel` call. If the error variable is declared but not used, a violation is reported. @@ -24,6 +23,7 @@ This rule is not entirely redundant to `noUnusedLocals` - it is made in mind to ## Options - `ajelAlias` (default: 'ajel'): Specify the alias for the ajel method. This allows you to customize the method name if it differs from the default 'ajel'. +- `sjelAlias` (default: 'sjel'): Specify the alias for the sjel method. This allows you to customize the method name if it differs from the default 'sjel'. ## Implementation @@ -33,6 +33,7 @@ This rule is not entirely redundant to `noUnusedLocals` - it is made in mind to ## Examples ```javascript +// ajel // Bad: Declaring error variable but not using it const [data, err] = await ajel(Promise.resolve(1)); // 'err' should be handled, e.g., by logging or other usage @@ -40,6 +41,20 @@ const [data, err] = await ajel(Promise.resolve(1)); // Good: Properly handling the error variable const [data, err] = await ajel(Promise.resolve(1)); +if (err) { + console.error(err); +} + +// ----- + +// sjel +// Bad: Declaring error variable but not using it +const [data, err] = sjel(fs.readFileSync)(path, { encoding: 'utf8' }); +// 'err' should be handled, e.g., by logging or other usage + +// Good: Properly handling the error variable +const [data, err] = sjel(fs.readFileSync)(path, { encoding: 'utf8' }); + if (err) { console.error(err); } diff --git a/apps/docs/src/pages/rules/ajel-require-tuple-declaration.md b/apps/docs/src/pages/rules/ajel-require-tuple-declaration.md index b31b774..45fff12 100644 --- a/apps/docs/src/pages/rules/ajel-require-tuple-declaration.md +++ b/apps/docs/src/pages/rules/ajel-require-tuple-declaration.md @@ -19,6 +19,7 @@ This rule assists developers towards the proper usage of ajel. ## Options - `ajelAlias` (default: 'ajel'): Specify the alias for the ajel method. This allows you to customize the method name if it differs from the default 'ajel'. +- `sjelAlias` (default: 'sjel'): Specify the alias for the sjel method. This allows you to customize the method name if it differs from the default 'sjel'. ## Implementation @@ -28,6 +29,7 @@ This rule assists developers towards the proper usage of ajel. ## Examples ```javascript +// ajel // Bad: Incorrect structure const [value] = await ajel(Promise.resolve(1)); // Violation: Tuple must have exactly 2 elements @@ -36,4 +38,16 @@ const err = await ajel(Promise.resolve(1)); // Violation: Limit declarations to // Good: Correct tuple declaration const [data, err] = await ajel(Promise.resolve(1)); + +// ----- + +// sjel +// Bad: Incorrect structure +const [value] = sjel(() => 1)(); // Violation: Tuple must have exactly 2 elements + +// Bad: Incorrect structure +const err = sjel(() => 1)(); // Violation: Limit declarations to a tuple + +// Good: Correct tuple declaration +const [data, err] = sjel(() => 1)(); ``` diff --git a/apps/docs/src/pages/rules/ajel-strict-error-instanceof.md b/apps/docs/src/pages/rules/ajel-strict-error-instanceof.md index d215dee..55ae19e 100644 --- a/apps/docs/src/pages/rules/ajel-strict-error-instanceof.md +++ b/apps/docs/src/pages/rules/ajel-strict-error-instanceof.md @@ -17,6 +17,7 @@ Properly narrowing the error type with `instanceof` promotes more precise error ## Options - `ajelAlias` (default: 'ajel'): Specify the alias for the ajel method. This allows you to customize the method name if it differs from the default 'ajel'. +- `sjelAlias` (default: 'sjel'): Specify the alias for the sjel method. This allows you to customize the method name if it differs from the default 'sjel'. ## Implementation @@ -26,6 +27,7 @@ Properly narrowing the error type with `instanceof` promotes more precise error ## Examples ```javascript +// ajel // Bad: Error variable not narrowed with instanceof const [data, err] = await ajel(Promise.resolve(1)); // Missing instanceof check for specific error type @@ -39,4 +41,21 @@ const [data, err] = await ajel(Promise.resolve(1)); if (err instanceof SpecificError) { console.error(err.message); } + +// ----- + +// sjel +// Bad: Error variable not narrowed with instanceof +const [data, err] = sjel(() => 1)(); +// Missing instanceof check for specific error type +if (err) { + console.error(err); +} + +// Good: Error variable narrowed with instanceof +const [data, err] = sjel(() => 1)(); +// Handle specific error type +if (err instanceof SpecificError) { + console.error(err.message); +} ``` diff --git a/apps/docs/src/pages/rules/sjel-require-currying.md b/apps/docs/src/pages/rules/sjel-require-currying.md new file mode 100644 index 0000000..b7f6c8d --- /dev/null +++ b/apps/docs/src/pages/rules/sjel-require-currying.md @@ -0,0 +1,32 @@ +# ajel/sjel-require-currying + +> This rule provides clearer errors when trying to use sjel without currying. This should help more junior eyes. + +- ⭐️ This rule is included in `plugin:ajel/recommended` preset. + +## Summary + +The `sjel-require-currying` rule provides a clear error that when calling sjel, you should be currying. + +## Rule Details + +- This rule checks for Variable Declarations that include a call to the sjel method. If a variable declaration does not find a AST nodes that denote currying, an error is reported. + +## Options + +- `sjelAlias` (default: 'sjel'): Specify the alias for the sjel method. This allows you to customize the method name if it differs from the default 'sjel'. + +## Implementation + +- [Rule source](https://github.com/Handfish/ajel/blob/main/packages/eslint-plugin-ajel/src/rules/sjel-require-currying.ts) +- [Test source](https://github.com/Handfish/ajel/blob/main/packages/eslint-plugin-ajel/tests/rules/sjel-require-currying.ts) + +## Examples + +```javascript +// Bad: calling sjel without currying (will provide 2 errors - a type error and our custom error) +let [res, err] = sjel(() => null); + +// Good: calling sjel and currying +const [res, err] = sjel(() => null)(); +``` diff --git a/packages/ajel-core/package.json b/packages/ajel-core/package.json index 75f9f5d..8a54638 100644 --- a/packages/ajel-core/package.json +++ b/packages/ajel-core/package.json @@ -1,7 +1,7 @@ { "name": "ajel", - "description": "Ajel allows you to handle errors similarly to Golang.", - "version": "0.0.7", + "description": "ajel encourages to handle errors similarly to Golang", + "version": "0.0.8", "main": "./dist/index.js", "module": "./dist/index.mjs", "types": "./dist/index.d.ts", diff --git a/packages/ajel-core/readme.md b/packages/ajel-core/readme.md index edbc123..88cd380 100644 --- a/packages/ajel-core/readme.md +++ b/packages/ajel-core/readme.md @@ -1,6 +1,6 @@

Laravel Logo

-

Ajel is a 133 byte function that allows you to handle errors in a way that is similar to Golang.

+

Ajel is a 312 byte set of functions that encourage you to handle errors in a way that is similar to Golang.

NPM Version @@ -12,7 +12,7 @@ -# [ajel (Asynchronous Javascript Error Library)](https://handfish.github.io/ajel) +# [ajel](https://handfish.github.io/ajel) ### Installation @@ -23,6 +23,7 @@ ### Example usage ```typescript +// Handling async functions that throw import { ajel } from 'ajel'; async function main() { @@ -36,6 +37,21 @@ async function main() { } ``` +```typescript +// Handling async functions that throw +import { sjel } from 'ajel'; + +async function main() { + const [res, err] = sjel(fs.readFileSync)(path, { encoding: 'utf8' }); + + if (err) { + return err; + } + + return result; +} +``` + `ajel` is a function that consumes a promise and returns a tuple representing the result and the error. On success, the result item has value. On error, the error item has value. It's that simple. diff --git a/packages/ajel-core/src/index.ts b/packages/ajel-core/src/index.ts index a64b403..17e6ac9 100644 --- a/packages/ajel-core/src/index.ts +++ b/packages/ajel-core/src/index.ts @@ -1 +1,2 @@ export { ajel } from "./ajel"; +export { sjel } from "./sjel"; diff --git a/packages/ajel-core/src/sjel.ts b/packages/ajel-core/src/sjel.ts new file mode 100644 index 0000000..8388eb1 --- /dev/null +++ b/packages/ajel-core/src/sjel.ts @@ -0,0 +1,10 @@ +type AnyFunction = (...args: any[]) => any; + +export const sjel = (fn: AnyFunction) => (...args: any[]) => { + try { + const result = fn(...args); + return [result, undefined] as [result: Result, error?: undefined]; + } catch (error) { + return [undefined, error] as [result: undefined, error: unknown]; + } +}; diff --git a/packages/benchmarks/README.md b/packages/benchmarks/README.md index 2bca75b..6a169d7 100644 --- a/packages/benchmarks/README.md +++ b/packages/benchmarks/README.md @@ -35,4 +35,15 @@ Allocate 500kb Files 2 times │ 1 │ 'ajel' │ '2,297' │ 435230.6347826253 │ '±8.77%' │ 230 │ │ 2 │ 'neverthrow' │ '2,417' │ 413718.4740590656 │ '±9.86%' │ 242 │ └─────────┴──────────────┴─────────┴───────────────────┴──────────┴─────────┘ + +------ +sjel + +Allocate 500kb Files 2 times +┌─────────┬─────────────┬─────────┬────────────────────┬──────────┬─────────┐ +│ (index) │ Task Name │ ops/sec │ Average Time (ns) │ Margin │ Samples │ +├─────────┼─────────────┼─────────┼────────────────────┼──────────┼─────────┤ +│ 0 │ 'try_catch' │ '533' │ 1873371.6187653719 │ '±6.23%' │ 54 │ +│ 1 │ 'sjel' │ '528' │ 1890484.647930793 │ '±8.25%' │ 53 │ +└─────────┴─────────────┴─────────┴────────────────────┴──────────┴─────────┘ ``` diff --git a/packages/benchmarks/async_tests.ts b/packages/benchmarks/async_tests.ts index 8ca6473..df4e514 100644 --- a/packages/benchmarks/async_tests.ts +++ b/packages/benchmarks/async_tests.ts @@ -26,14 +26,13 @@ function fibonacci(n: number): number { } -// Fibonacci calculation (CPU-bound task) async function createAndDeleteFilesAsync(n: number, nameToken: string): Promise { return new Promise((resolve) => { resolve(createAndDeleteFiles(n, nameToken)); }); } -export async function tryAsyncGoFib(n: number) { +export async function tryAjelFib(n: number) { const [res, err] = await ajel( calculateFibonacciAsync(n) ); @@ -87,7 +86,7 @@ function createAndDeleteFiles(i: number, nameToken: string) { return 1; } -export async function tryAsyncGoFile(n: number) { +export async function tryAjelFile(n: number) { const [res, err] = await ajel( createAndDeleteFilesAsync(n, "A") ); diff --git a/packages/benchmarks/benchmark.ts b/packages/benchmarks/benchmark.ts index ee790eb..565ce21 100644 --- a/packages/benchmarks/benchmark.ts +++ b/packages/benchmarks/benchmark.ts @@ -1,5 +1,6 @@ import { Bench } from 'tinybench'; import * as at from './async_tests.ts'; +import * as st from './sync_tests.ts'; const bench = new Bench({ time: 100 }); @@ -8,7 +9,7 @@ bench await at.tryTryCatchFib(2) }) .add('ajel', async function () { - await at.tryAsyncGoFib(2) + await at.tryAjelFib(2) }) .add('neverthrow', async function () { await at.tryNeverthrowFib(2) @@ -31,7 +32,7 @@ bench await at.tryTryCatchFib(20) }) .add('ajel', async function () { - await at.tryAsyncGoFib(20) + await at.tryAjelFib(20) }) .add('neverthrow', async function () { await at.tryNeverthrowFib(20) @@ -55,7 +56,7 @@ bench await at.tryTryCatchFile(2) }) .add('ajel', async function () { - await at.tryAsyncGoFile(2) + await at.tryAjelFile(2) }) .add('neverthrow', async function () { await at.tryNeverthrowFile(2) @@ -64,3 +65,27 @@ bench console.log("Allocate 500kb Files 2 times") await bench.run(); console.table(bench.table()); + + +//------ +console.log("") +console.log("------") +console.log("sjel") +console.log("") + +bench + .remove('try_catch') + .remove('ajel') + .remove('neverthrow') + +bench + .add('try_catch', async function () { + await st.trySjelFile(2) + }) + .add('sjel', async function () { + await at.tryTryCatchFile(2) + }) + +console.log("Allocate 500kb Files 2 times") +await bench.run(); +console.table(bench.table()); diff --git a/packages/benchmarks/sync_tests.ts b/packages/benchmarks/sync_tests.ts new file mode 100644 index 0000000..7e41657 --- /dev/null +++ b/packages/benchmarks/sync_tests.ts @@ -0,0 +1,55 @@ +import fs from 'fs'; + +type AnyFunction = (...args: any[]) => any; + +export const sjel = (fn: AnyFunction) => (...args: any[]) => { + try { + const result = fn(...args); + return [result, undefined] as [result: Result, error?: undefined]; + } catch (error) { + return [undefined, error] as [result: undefined, error: unknown]; + } +}; + +//--- +// File test implementation +function createAndDeleteFiles(i: number, nameToken: string) { + for (let count = 0; count < i; count++) { + // Generate random content for the file + const content = Buffer.alloc(500 * 1024, 'a'); // 500KB of 'a' + + // Write the file + const fileName = `file_${nameToken}.txt`; + fs.writeFileSync(fileName, content); + + // Delete the file + fs.unlinkSync(fileName); + } + + return 1; +} + +export async function trySjelFile(n: number) { + const [res, err] = sjel( + createAndDeleteFiles + )(n, "A"); + if (err instanceof Error && !res) { + return err + } + + return res; +}; + +export async function tryTryCatchFile(n: number) { + let res: number; + + try { + res = createAndDeleteFiles(n, "B") + } + catch (e) { + return e + } + + return res; +}; + diff --git a/packages/eslint-plugin-ajel/README.md b/packages/eslint-plugin-ajel/README.md index 3881e85..f16181f 100644 --- a/packages/eslint-plugin-ajel/README.md +++ b/packages/eslint-plugin-ajel/README.md @@ -1,6 +1,6 @@

Laravel Logo

-

Ajel is a 133 byte function that allows you to handle errors in a way that is similar to Golang.

+

Ajel is a 312 byte set of functions that encourage you to handle errors in a way that is similar to Golang.

NPM Version @@ -10,7 +10,7 @@

-# [ajel (Asynchronous Javascript Error Library)](https://handfish.github.io/ajel) +# [ajel](https://handfish.github.io/ajel) ### Installation @@ -42,26 +42,42 @@ 'error', { ajelAlias: "blimpy", + sjelAlias: "limpyb", }, ], - 'ajel/ajel-require-error-handling': [ + 'ajel/ajel-require-tuple-declaration': [ 'error', { ajelAlias: 'blimpy', + sjelAlias: "limpyb", }, ], - 'ajel/ajel-require-tuple-declaration': [ + 'ajel/ajel-disable-try-catch': [ 'error', { ajelAlias: 'blimpy', + sjelAlias: "limpyb", }, - ], - 'ajel/ajel-disable-try-catch': [ + 'ajel/sjel-require-currying': [ + 'error', + { + sjelAlias: "limpyb", + }, + //Use one of the two following rules + 'ajel/ajel-require-error-handling': [ 'error', { ajelAlias: 'blimpy', + sjelAlias: "limpyb", }, ], + //'ajel/ajel-strict-error-instanceof': [ + // 'off', + // { + // ajelAlias: 'blimpy', + // sjelAlias: "limpyb", + // }, + ], }, } ``` diff --git a/packages/eslint-plugin-ajel/docs/rules/ajel-const-tuples.md b/packages/eslint-plugin-ajel/docs/rules/ajel-const-tuples.md index eda890a..488c0e8 100644 --- a/packages/eslint-plugin-ajel/docs/rules/ajel-const-tuples.md +++ b/packages/eslint-plugin-ajel/docs/rules/ajel-const-tuples.md @@ -15,18 +15,29 @@ This rule checks for Variable Declarations that include a call to the ajel metho ## Options - `ajelAlias` (default: 'ajel'): Specify the alias for the ajel method. This allows you to customize the method name if it differs from the default 'ajel'. +- `sjelAlias` (default: 'sjel'): Specify the alias for the sjel method. This allows you to customize the method name if it differs from the default 'sjel'. ## Implementation -- [Rule source](../../src/rules/ajel-const-tuples.ts) -- [Test source](../../tests/rules/ajel-const-tuples.ts) +- [Rule source](https://github.com/Handfish/ajel/blob/main/packages/eslint-plugin-ajel/src/rules/ajel-const-tuples.ts) +- [Test source](https://github.com/Handfish/ajel/blob/main/packages/eslint-plugin-ajel/tests/rules/ajel-const-tuples.ts) ## Examples ```javascript +// ajel // Bad: Variable declaration without 'const' for ajel method let [res, err] = await ajel(Promise.resolve(1)); // Good: Variable declaration with 'const' for ajel method const [res, err] = await ajel(Promise.resolve(1)); + +// ----- + +// sjel +// Bad: Variable declaration without 'const' for sjel method +let [res, err] = sjel(() => 1)(); + +// Good: Variable declaration with 'const' for sjel method +const [res, err] = sjel(() => 1)(); ``` diff --git a/packages/eslint-plugin-ajel/docs/rules/ajel-disable-throw.md b/packages/eslint-plugin-ajel/docs/rules/ajel-disable-throw.md index 45ce953..a6a8494 100644 --- a/packages/eslint-plugin-ajel/docs/rules/ajel-disable-throw.md +++ b/packages/eslint-plugin-ajel/docs/rules/ajel-disable-throw.md @@ -22,8 +22,8 @@ This rule does not have any configurable options. ## Implementation -- [Rule source](../../src/rules/ajel-disable-throw.ts) -- [Test source](../../tests/rules/ajel-disable-throw.ts) +- [Rule source](https://github.com/Handfish/ajel/blob/main/packages/eslint-plugin-ajel/src/rules/ajel-disable-throw.ts) +- [Test source](https://github.com/Handfish/ajel/blob/main/packages/eslint-plugin-ajel/tests/rules/ajel-disable-throw.ts) ## Examples diff --git a/packages/eslint-plugin-ajel/docs/rules/ajel-disable-try-catch.md b/packages/eslint-plugin-ajel/docs/rules/ajel-disable-try-catch.md index 91b3b4c..18c4976 100644 --- a/packages/eslint-plugin-ajel/docs/rules/ajel-disable-try-catch.md +++ b/packages/eslint-plugin-ajel/docs/rules/ajel-disable-try-catch.md @@ -19,15 +19,17 @@ The use of try-catch blocks does not align with preferred error-handling strateg ## Options - `ajelAlias` (default: 'ajel'): Specify the alias for the ajel method. This allows you to customize the method name if it differs from the default 'ajel'. +- `sjelAlias` (default: 'sjel'): Specify the alias for the sjel method. This allows you to customize the method name if it differs from the default 'sjel'. ## Implementation -- [Rule source](../../src/rules/ajel-disable-try-catch.ts) -- [Test source](../../tests/rules/ajel-disable-try-catch.ts) +- [Rule source](https://github.com/Handfish/ajel/blob/main/packages/eslint-plugin-ajel/src/rules/ajel-disable-try-catch.ts) +- [Test source](https://github.com/Handfish/ajel/blob/main/packages/eslint-plugin-ajel/tests/rules/ajel-disable-try-catch.ts) ## Examples ```javascript +// ajel // Bad: Using TryStatement for error handling let res; @@ -41,5 +43,17 @@ try { // Better: Utilizing ajel method for error handling const [res, err] = await ajel(dangerousOperation()); -if (err) return err; +// ----- + +// sjel +// Bad: Using TryStatement for error handling +try { + res = foo; + JSON.parse("{'badjson`: 'asd'}"); +} catch (error) { + return error; +} + +// Better: Utilizing sjel method for error handling +const [res, err] = sjel(JSON.parse)("{'badjson`: 'asd'}"); ``` diff --git a/packages/eslint-plugin-ajel/docs/rules/ajel-require-error-handling.md b/packages/eslint-plugin-ajel/docs/rules/ajel-require-error-handling.md index b7f8ab1..b851cec 100644 --- a/packages/eslint-plugin-ajel/docs/rules/ajel-require-error-handling.md +++ b/packages/eslint-plugin-ajel/docs/rules/ajel-require-error-handling.md @@ -14,27 +14,47 @@ This rule checks for the usage of the error variable (`err`) in the context of a ## Why is this Rule Useful? +> Your leading underscores won't save you now. + Handling errors properly is crucial for robust and maintainable code. This rule encourages developers to explicitly handle errors returned from `ajel` calls, reducing the risk of silent failures and improving the overall reliability of the codebase. +This rule is not entirely redundant to `noUnusedLocals` - it is made in mind to be utilized simultaneously. This rule is meant to still throw errors when a developer uses a leading underscore to mark the variable as local only. + ## Options - `ajelAlias` (default: 'ajel'): Specify the alias for the ajel method. This allows you to customize the method name if it differs from the default 'ajel'. +- `sjelAlias` (default: 'sjel'): Specify the alias for the sjel method. This allows you to customize the method name if it differs from the default 'sjel'. ## Implementation -- [Rule source](../../src/rules/ajel-require-error-handling.ts) -- [Test source](../../tests/rules/ajel-require-error-handling.ts) +- [Rule source](https://github.com/Handfish/ajel/blob/main/packages/eslint-plugin-ajel/src/rules/ajel-require-error-handling.ts) +- [Test source](https://github.com/Handfish/ajel/blob/main/packages/eslint-plugin-ajel/tests/rules/ajel-require-error-handling.ts) ## Examples ```javascript +// ajel // Bad: Declaring error variable but not using it const [data, err] = await ajel(Promise.resolve(1)); -// 'err' should be handled, e.g., by logging or throwing +// 'err' should be handled, e.g., by logging or other usage // Good: Properly handling the error variable const [data, err] = await ajel(Promise.resolve(1)); +if (err) { + console.error(err); +} + +// ----- + +// sjel +// Bad: Declaring error variable but not using it +const [data, err] = sjel(fs.readFileSync)(path, { encoding: 'utf8' }); +// 'err' should be handled, e.g., by logging or other usage + +// Good: Properly handling the error variable +const [data, err] = sjel(fs.readFileSync)(path, { encoding: 'utf8' }); + if (err) { console.error(err); } diff --git a/packages/eslint-plugin-ajel/docs/rules/ajel-require-tuple-declaration.md b/packages/eslint-plugin-ajel/docs/rules/ajel-require-tuple-declaration.md index 980f404..45fff12 100644 --- a/packages/eslint-plugin-ajel/docs/rules/ajel-require-tuple-declaration.md +++ b/packages/eslint-plugin-ajel/docs/rules/ajel-require-tuple-declaration.md @@ -19,15 +19,17 @@ This rule assists developers towards the proper usage of ajel. ## Options - `ajelAlias` (default: 'ajel'): Specify the alias for the ajel method. This allows you to customize the method name if it differs from the default 'ajel'. +- `sjelAlias` (default: 'sjel'): Specify the alias for the sjel method. This allows you to customize the method name if it differs from the default 'sjel'. ## Implementation -- [Rule source](../../src/rules/ajel-require-tuple-declaration.ts) -- [Test source](../../tests/rules/ajel-require-tuple-declaration.ts) +- [Rule source](https://github.com/Handfish/ajel/blob/main/packages/eslint-plugin-ajel/src/rules/ajel-require-tuple-declaration.ts) +- [Test source](https://github.com/Handfish/ajel/blob/main/packages/eslint-plugin-ajel/tests/rules/ajel-require-tuple-declaration.ts) ## Examples ```javascript +// ajel // Bad: Incorrect structure const [value] = await ajel(Promise.resolve(1)); // Violation: Tuple must have exactly 2 elements @@ -36,4 +38,16 @@ const err = await ajel(Promise.resolve(1)); // Violation: Limit declarations to // Good: Correct tuple declaration const [data, err] = await ajel(Promise.resolve(1)); + +// ----- + +// sjel +// Bad: Incorrect structure +const [value] = sjel(() => 1)(); // Violation: Tuple must have exactly 2 elements + +// Bad: Incorrect structure +const err = sjel(() => 1)(); // Violation: Limit declarations to a tuple + +// Good: Correct tuple declaration +const [data, err] = sjel(() => 1)(); ``` diff --git a/packages/eslint-plugin-ajel/docs/rules/ajel-strict-error-instanceof.md b/packages/eslint-plugin-ajel/docs/rules/ajel-strict-error-instanceof.md index d73e323..55ae19e 100644 --- a/packages/eslint-plugin-ajel/docs/rules/ajel-strict-error-instanceof.md +++ b/packages/eslint-plugin-ajel/docs/rules/ajel-strict-error-instanceof.md @@ -2,8 +2,6 @@ > This rule enforces narrowing the error type with `instanceof`. -- ⭐️ This rule is included in `plugin:ajel/recommended` preset. - ## Summary The `ajel-strict-error-instanceof` rule ensures that when calling `ajel`, the error variable is properly narrowed using the `instanceof` operator. This helps developers perform more specific error handling and enhances the type safety of the code. @@ -19,15 +17,17 @@ Properly narrowing the error type with `instanceof` promotes more precise error ## Options - `ajelAlias` (default: 'ajel'): Specify the alias for the ajel method. This allows you to customize the method name if it differs from the default 'ajel'. +- `sjelAlias` (default: 'sjel'): Specify the alias for the sjel method. This allows you to customize the method name if it differs from the default 'sjel'. ## Implementation -- [Rule source](../../src/rules/ajel-strict-error-instanceof.ts) -- [Test source](../../tests/rules/ajel-strict-error-instanceof.ts) +- [Rule source](https://github.com/Handfish/ajel/blob/main/packages/eslint-plugin-ajel/src/rules/ajel-strict-error-instanceof.ts) +- [Test source](https://github.com/Handfish/ajel/blob/main/packages/eslint-plugin-ajel/tests/rules/ajel-strict-error-instanceof.ts) ## Examples ```javascript +// ajel // Bad: Error variable not narrowed with instanceof const [data, err] = await ajel(Promise.resolve(1)); // Missing instanceof check for specific error type @@ -41,4 +41,21 @@ const [data, err] = await ajel(Promise.resolve(1)); if (err instanceof SpecificError) { console.error(err.message); } + +// ----- + +// sjel +// Bad: Error variable not narrowed with instanceof +const [data, err] = sjel(() => 1)(); +// Missing instanceof check for specific error type +if (err) { + console.error(err); +} + +// Good: Error variable narrowed with instanceof +const [data, err] = sjel(() => 1)(); +// Handle specific error type +if (err instanceof SpecificError) { + console.error(err.message); +} ``` diff --git a/packages/eslint-plugin-ajel/docs/rules/sjel-require-currying.md b/packages/eslint-plugin-ajel/docs/rules/sjel-require-currying.md new file mode 100644 index 0000000..b7f6c8d --- /dev/null +++ b/packages/eslint-plugin-ajel/docs/rules/sjel-require-currying.md @@ -0,0 +1,32 @@ +# ajel/sjel-require-currying + +> This rule provides clearer errors when trying to use sjel without currying. This should help more junior eyes. + +- ⭐️ This rule is included in `plugin:ajel/recommended` preset. + +## Summary + +The `sjel-require-currying` rule provides a clear error that when calling sjel, you should be currying. + +## Rule Details + +- This rule checks for Variable Declarations that include a call to the sjel method. If a variable declaration does not find a AST nodes that denote currying, an error is reported. + +## Options + +- `sjelAlias` (default: 'sjel'): Specify the alias for the sjel method. This allows you to customize the method name if it differs from the default 'sjel'. + +## Implementation + +- [Rule source](https://github.com/Handfish/ajel/blob/main/packages/eslint-plugin-ajel/src/rules/sjel-require-currying.ts) +- [Test source](https://github.com/Handfish/ajel/blob/main/packages/eslint-plugin-ajel/tests/rules/sjel-require-currying.ts) + +## Examples + +```javascript +// Bad: calling sjel without currying (will provide 2 errors - a type error and our custom error) +let [res, err] = sjel(() => null); + +// Good: calling sjel and currying +const [res, err] = sjel(() => null)(); +``` diff --git a/packages/eslint-plugin-ajel/package.json b/packages/eslint-plugin-ajel/package.json index a137851..8a0cabb 100644 --- a/packages/eslint-plugin-ajel/package.json +++ b/packages/eslint-plugin-ajel/package.json @@ -1,7 +1,7 @@ { "name": "eslint-plugin-ajel", - "version": "0.0.7", - "description": "Eslint rules for Ajel, the library that allows you to handle errors similarly to Golang", + "version": "0.0.8", + "description": "Eslint rules for ajel, the library encourages you to handle errors similarly to Golang", "engines": { "node": ">=8.10.0" }, diff --git a/packages/eslint-plugin-ajel/src/configs/recommended.ts b/packages/eslint-plugin-ajel/src/configs/recommended.ts index d2a6c6e..6b8fee3 100644 --- a/packages/eslint-plugin-ajel/src/configs/recommended.ts +++ b/packages/eslint-plugin-ajel/src/configs/recommended.ts @@ -8,5 +8,6 @@ export const recommended = { 'ajel/ajel-require-tuple-declaration': 'error', 'ajel/ajel-require-error-handling': 'error', 'ajel/ajel-strict-error-instanceof': 'off', + 'ajel/sjel-require-currying': 'error', }, }; diff --git a/packages/eslint-plugin-ajel/src/index.ts b/packages/eslint-plugin-ajel/src/index.ts index 1e5b341..8722a01 100644 --- a/packages/eslint-plugin-ajel/src/index.ts +++ b/packages/eslint-plugin-ajel/src/index.ts @@ -6,6 +6,7 @@ import ajelDisableTryCatch from './rules/ajel-disable-try-catch'; import ajelRequireTupleDeclaration from './rules/ajel-require-tuple-declaration'; import ajelStrictErrorInstanceof from './rules/ajel-strict-error-instanceof'; import ajelRequireErrorHandling from './rules/ajel-require-error-handling'; +import sjelRequireCurrying from './rules/sjel-require-currying'; export const configs = { recommended, @@ -18,4 +19,5 @@ export const rules = { 'ajel-require-tuple-declaration': ajelRequireTupleDeclaration, 'ajel-strict-error-instanceof': ajelStrictErrorInstanceof, 'ajel-require-error-handling': ajelRequireErrorHandling, + 'sjel-require-currying': sjelRequireCurrying, }; diff --git a/packages/eslint-plugin-ajel/src/rules/ajel-const-tuples.ts b/packages/eslint-plugin-ajel/src/rules/ajel-const-tuples.ts index 7bd0a59..9f2cfdf 100644 --- a/packages/eslint-plugin-ajel/src/rules/ajel-const-tuples.ts +++ b/packages/eslint-plugin-ajel/src/rules/ajel-const-tuples.ts @@ -5,7 +5,8 @@ import { hasAjelCallExpressionChild } from '../utils/hasAjelCallExpressionChild' type Options = [ { ajelAlias?: string; - } + sjelAlias?: string; + }, ]; type MessageIds = 'const'; @@ -14,7 +15,7 @@ const rule = createRule({ meta: { type: 'problem', docs: { - description: 'This rule enforces the use of a const calling ajel', + description: 'This rule enforces the use of a const calling ajel or sjel', recommended: 'error', }, schema: [ @@ -24,30 +25,41 @@ const rule = createRule({ ajelAlias: { type: 'string', }, + sjelAlias: { + type: 'string', + }, }, additionalProperties: false, }, ], messages: { - const: "Utilize 'const' when calling {{ajelAlias}}", + const: "Utilize 'const' when calling {{ajelOrSjel}}", }, }, defaultOptions: [ { ajelAlias: 'ajel', + sjelAlias: 'sjel', }, ], create: (context, [options]) => { return { VariableDeclaration(node: TSESTree.VariableDeclaration): void { - if (hasAjelCallExpressionChild(node, options.ajelAlias)) { + const [hasAjelCallExpression, ajelOrSjel] = hasAjelCallExpressionChild( + node, + options.ajelAlias, + options.sjelAlias + ); + + if (hasAjelCallExpression && ajelOrSjel) { if (node.kind !== 'const') { context.report({ node, messageId: 'const', data: { - ajelAlias: options.ajelAlias, + ajelOrSjel: + ajelOrSjel === 'ajel' ? options.ajelAlias : options.sjelAlias, }, }); } diff --git a/packages/eslint-plugin-ajel/src/rules/ajel-disable-try-catch.ts b/packages/eslint-plugin-ajel/src/rules/ajel-disable-try-catch.ts index 903cb2f..5a8260c 100644 --- a/packages/eslint-plugin-ajel/src/rules/ajel-disable-try-catch.ts +++ b/packages/eslint-plugin-ajel/src/rules/ajel-disable-try-catch.ts @@ -10,7 +10,8 @@ export function isTryStatement( type Options = [ { ajelAlias?: string; - } + sjelAlias?: string; + }, ]; type MessageIds = 'tryStatement'; @@ -30,21 +31,26 @@ const rule = createRule({ ajelAlias: { type: 'string', }, + sjelAlias: { + type: 'string', + }, }, additionalProperties: false, }, ], messages: { - tryStatement: 'Consider using {{ajelAlias}} instead of try catch', + tryStatement: + 'Consider using {{ajelAlias}} or {{sjelAlias}} instead of try catch', }, }, defaultOptions: [ { ajelAlias: 'ajel', + sjelAlias: 'sjel', }, ], - create: (context, [{ ajelAlias }]) => { + create: (context, [{ ajelAlias, sjelAlias }]) => { return { TryStatement: (node: TSESTree.TryStatement) => { // Check if it's a TryStatement @@ -55,6 +61,7 @@ const rule = createRule({ messageId: 'tryStatement', data: { ajelAlias, + sjelAlias, }, }); } diff --git a/packages/eslint-plugin-ajel/src/rules/ajel-require-error-handling.ts b/packages/eslint-plugin-ajel/src/rules/ajel-require-error-handling.ts index 4d2fa27..699351e 100644 --- a/packages/eslint-plugin-ajel/src/rules/ajel-require-error-handling.ts +++ b/packages/eslint-plugin-ajel/src/rules/ajel-require-error-handling.ts @@ -6,6 +6,7 @@ import { isUsedVariable } from '../utils/isUsed'; type Options = [ { ajelAlias?: string; + sjelAlias?: string; }, ]; type MessageIds = 'requireErrorHandling'; @@ -25,22 +26,27 @@ const rule = createRule({ ajelAlias: { type: 'string', }, + sjelAlias: { + type: 'string', + }, }, additionalProperties: false, }, ], messages: { - requireErrorHandling: 'err from {{ajelAlias}} must be used', + requireErrorHandling: 'err from {{ajelOrSjel}} must be used', }, }, defaultOptions: [ { ajelAlias: 'ajel', + sjelAlias: 'sjel', }, ], - create: (context, [{ ajelAlias }]) => { + create: (context, [{ ajelAlias, sjelAlias }]) => { let errorVariable: TSESTree.Identifier | null = null; + let ajelOrSjelReport: 'ajel' | 'sjel' | undefined = undefined; function reportIfErrorVariableUnused() { if (errorVariable) { @@ -57,7 +63,7 @@ const rule = createRule({ node: errorVariable, messageId: 'requireErrorHandling', data: { - ajelAlias, + ajelOrSjel: ajelOrSjelReport === 'ajel' ? ajelAlias : sjelAlias, }, }); } @@ -66,13 +72,20 @@ const rule = createRule({ return { VariableDeclaration(node: TSESTree.VariableDeclaration): void { - if (hasAjelCallExpressionChild(node, ajelAlias)) { + const [hasAjelCallExpression, ajelOrSjel] = hasAjelCallExpressionChild( + node, + ajelAlias, + sjelAlias + ); + + if (hasAjelCallExpression && ajelOrSjel) { const declarator = node.declarations[0]; if ( declarator.id.type === 'ArrayPattern' && declarator.id.elements[1]?.type === 'Identifier' ) { errorVariable = declarator.id.elements[1]; + ajelOrSjelReport = ajelOrSjel; } } }, diff --git a/packages/eslint-plugin-ajel/src/rules/ajel-require-tuple-declaration.ts b/packages/eslint-plugin-ajel/src/rules/ajel-require-tuple-declaration.ts index 9ce22c2..bbdbec5 100644 --- a/packages/eslint-plugin-ajel/src/rules/ajel-require-tuple-declaration.ts +++ b/packages/eslint-plugin-ajel/src/rules/ajel-require-tuple-declaration.ts @@ -5,7 +5,8 @@ import { hasAjelCallExpressionChild } from '../utils/hasAjelCallExpressionChild' type Options = [ { ajelAlias?: string; - } + sjelAlias?: string; + }, ]; type MessageIds = 'limitDeclarations' | 'tupleLen2'; @@ -25,31 +26,44 @@ const rule = createRule({ ajelAlias: { type: 'string', }, + sjelAlias: { + type: 'string', + }, }, additionalProperties: false, }, ], messages: { - limitDeclarations: 'Declare only the tuple when calling {{ajelAlias}}', + limitDeclarations: 'Declare only the tuple when calling {{ajelOrSjel}}', tupleLen2: - 'Variable declarations must be a tuple of length 2 when calling {{ajelAlias}}', + 'Variable declarations must be a tuple of length 2 when calling {{ajelOrSjel}}', }, }, defaultOptions: [ { ajelAlias: 'ajel', + sjelAlias: 'sjel', }, ], - create: (context, [{ ajelAlias }]) => { + create: (context, [{ ajelAlias, sjelAlias }]) => { return { VariableDeclaration(node: TSESTree.VariableDeclaration): void { - if (hasAjelCallExpressionChild(node, ajelAlias)) { + const [hasAjelCallExpression, ajelOrSjel] = hasAjelCallExpressionChild( + node, + ajelAlias, + sjelAlias + ); + + if (hasAjelCallExpression && ajelOrSjel) { // Check if there's more than one declarator if (node.declarations.length !== 1) { context.report({ node, messageId: 'limitDeclarations', + data: { + ajelOrSjel: ajelOrSjel === 'ajel' ? ajelAlias : sjelAlias, + }, }); } const declarator = node.declarations[0]; @@ -62,7 +76,7 @@ const rule = createRule({ node, messageId: 'tupleLen2', data: { - ajelAlias, + ajelOrSjel: ajelOrSjel === 'ajel' ? ajelAlias : sjelAlias, }, }); } diff --git a/packages/eslint-plugin-ajel/src/rules/ajel-strict-error-instanceof.ts b/packages/eslint-plugin-ajel/src/rules/ajel-strict-error-instanceof.ts index a02457a..3332065 100644 --- a/packages/eslint-plugin-ajel/src/rules/ajel-strict-error-instanceof.ts +++ b/packages/eslint-plugin-ajel/src/rules/ajel-strict-error-instanceof.ts @@ -6,7 +6,8 @@ import { isFoundInBinaryExpressionWithInstanceOf } from '../utils/isFoundInBinar type Options = [ { ajelAlias?: string; - } + sjelAlias?: string; + }, ]; type MessageIds = 'instanceofError'; @@ -26,32 +27,44 @@ const rule = createRule({ ajelAlias: { type: 'string', }, + sjelAlias: { + type: 'string', + }, }, additionalProperties: false, }, ], messages: { - instanceofError: 'Utilize err instanceof', + instanceofError: 'Utilize err instanceof when using {{ajelOrSjel}}', }, }, defaultOptions: [ { ajelAlias: 'ajel', + sjelAlias: 'sjel', }, ], - create: (context, [{ ajelAlias }]) => { + create: (context, [{ ajelAlias, sjelAlias }]) => { let errorVariable: TSESTree.Identifier | null = null; + let ajelOrSjelReport: 'ajel' | 'sjel' | undefined = undefined; return { VariableDeclaration(node: TSESTree.VariableDeclaration): void { - if (hasAjelCallExpressionChild(node, ajelAlias)) { + const [hasAjelCallExpression, ajelOrSjel] = hasAjelCallExpressionChild( + node, + ajelAlias, + sjelAlias + ); + + if (hasAjelCallExpression && ajelOrSjel) { const declarator = node.declarations[0]; if ( declarator.id.type === 'ArrayPattern' && declarator.id.elements[1]?.type === 'Identifier' ) { errorVariable = declarator.id.elements[1]; + ajelOrSjelReport = ajelOrSjel; } } }, @@ -65,6 +78,9 @@ const rule = createRule({ context.report({ node: errorVariable, messageId: 'instanceofError', + data: { + ajelOrSjel: ajelOrSjelReport === 'ajel' ? ajelAlias : sjelAlias, + }, }); } }, diff --git a/packages/eslint-plugin-ajel/src/rules/sjel-require-currying.ts b/packages/eslint-plugin-ajel/src/rules/sjel-require-currying.ts new file mode 100644 index 0000000..c43a392 --- /dev/null +++ b/packages/eslint-plugin-ajel/src/rules/sjel-require-currying.ts @@ -0,0 +1,65 @@ +import { TSESTree } from '@typescript-eslint/utils'; +import { createRule } from '../utils/createRule'; + +type Options = [ + { + sjelAlias?: string; + }, +]; +type MessageIds = 'currying'; + +const rule = createRule({ + name: 'sjel-require-currying', + meta: { + type: 'problem', + docs: { + description: 'This rule enforces the use currying when calling sjel', + recommended: 'error', + }, + schema: [ + { + type: 'object', + properties: { + sjelAlias: { + type: 'string', + }, + }, + additionalProperties: false, + }, + ], + messages: { + currying: 'Use currying when calling {{sjelAlias}}', + }, + }, + defaultOptions: [ + { + sjelAlias: 'sjel', + }, + ], + + create: (context, [options]) => { + return { + VariableDeclaration(node: TSESTree.VariableDeclaration): void { + for (const declarator of node.declarations) { + if ( + declarator.init && + declarator.init.type === 'CallExpression' && + declarator.init.callee.type === 'Identifier' && + declarator.init.callee.name === + (options.sjelAlias ? options.sjelAlias : 'sjel') + ) { + context.report({ + node, + messageId: 'currying', + data: { + sjelAlias: options.sjelAlias, + }, + }); + } + } + }, + }; + }, +}); + +export default rule; diff --git a/packages/eslint-plugin-ajel/src/utils/hasAjelCallExpressionChild.ts b/packages/eslint-plugin-ajel/src/utils/hasAjelCallExpressionChild.ts index a9895b8..0fb5e04 100644 --- a/packages/eslint-plugin-ajel/src/utils/hasAjelCallExpressionChild.ts +++ b/packages/eslint-plugin-ajel/src/utils/hasAjelCallExpressionChild.ts @@ -2,24 +2,37 @@ import { TSESTree } from '@typescript-eslint/utils'; export function hasAjelCallExpressionChild( node: TSESTree.VariableDeclaration | null | undefined, - alias?: string -): boolean { + ajelAlias?: string, + sjelAlias?: string +): [boolean, 'ajel' | 'sjel' | undefined] { if (!node || node.type !== 'VariableDeclaration') { - return false; + return [false, undefined]; } for (const declarator of node.declarations) { + // Check for ajel if ( declarator.init && declarator.init.type === 'AwaitExpression' && declarator.init.argument && declarator.init.argument.type === 'CallExpression' && declarator.init.argument.callee.type === 'Identifier' && - declarator.init.argument.callee.name === (alias ? alias : 'ajel') + declarator.init.argument.callee.name === (ajelAlias ? ajelAlias : 'ajel') ) { - return true; + return [true, 'ajel']; + } + + // Check for sjel + if ( + declarator.init && + declarator.init.type === 'CallExpression' && + declarator.init.callee.type === 'CallExpression' && + declarator.init.callee.callee.type === 'Identifier' && + declarator.init.callee.callee.name === (sjelAlias ? sjelAlias : 'sjel') + ) { + return [true, 'sjel']; } } - return false; + return [false, undefined]; } diff --git a/packages/eslint-plugin-ajel/src/utils/isFoundInBinaryExpression.ts b/packages/eslint-plugin-ajel/src/utils/isFoundInBinaryExpression.ts deleted file mode 100644 index d08ba5c..0000000 --- a/packages/eslint-plugin-ajel/src/utils/isFoundInBinaryExpression.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { TSESTree } from '@typescript-eslint/utils'; - -export function isFoundInBinaryExpression( - node: TSESTree.Node | null | undefined, - errorVariable: TSESTree.Identifier | null | undefined -): boolean { - if (!node || node.type !== 'BinaryExpression') { - return false; - } - - if ( - (node.left.type === 'Identifier' && - node.left.name === errorVariable?.name) || - (node.right.type === 'Identifier' && - node.right.name === errorVariable?.name) - ) { - return true; - } - - return false; -} diff --git a/packages/eslint-plugin-ajel/tests/rules/ajel-const-tuples.ts b/packages/eslint-plugin-ajel/tests/rules/ajel-const-tuples.ts index f795c27..a5bc55d 100644 --- a/packages/eslint-plugin-ajel/tests/rules/ajel-const-tuples.ts +++ b/packages/eslint-plugin-ajel/tests/rules/ajel-const-tuples.ts @@ -5,13 +5,26 @@ const tester = new RuleTester(); tester.run('ajel-const-tuples', rule, { valid: [ + //ajel { code: `const [res, err] = await ajel(Promise.resolve(1));` }, { code: `const [res, err] = await blimpy(Promise.resolve(1));`, - options: [{ ajelAlias: 'blimpy' }], + options: [{ sjelAlias: 'sjel', ajelAlias: 'blimpy' }], + }, + //sjel + { code: `const [res, err] = sjel(() => null)();` }, + { + code: `const [res, err] = limpyb(() => null)();`, + options: [{ sjelAlias: 'limpyb', ajelAlias: 'ajel' }], + }, + { code: `const [res, err] = sjel(Json.parse)("{}");` }, + { + code: `const [res, err] = limpyb(Json.parse)("{}");`, + options: [{ sjelAlias: 'limpyb', ajelAlias: 'ajel' }], }, ], invalid: [ + //ajel { code: ` let [a, b] = await ajel(Promise.resolve([1, 2])); @@ -21,5 +34,36 @@ tester.run('ajel-const-tuples', rule, { `, errors: [{ messageId: 'const' }], }, + { + code: ` + let [a, b] = await blimpy(Promise.resolve([1, 2])); + a = [1]; + b = 2; + console.log(a, b); + `, + options: [{ sjelAlias: 'sjel', ajelAlias: 'blimpy' }], + errors: [{ messageId: 'const' }], + }, + + //sjel + { + code: ` + let [a, b] = sjel(() => [1,2])(); + a = [1]; + b = 2; + console.log(a, b); + `, + errors: [{ messageId: 'const' }], + }, + { + code: ` + let [a, b] = limpyb(() => [1,2])(); + a = [1]; + b = 2; + console.log(a, b); + `, + options: [{ sjelAlias: 'limpyb', ajelAlias: 'ajel' }], + errors: [{ messageId: 'const' }], + }, ], }); diff --git a/packages/eslint-plugin-ajel/tests/rules/ajel-require-error-handling.ts b/packages/eslint-plugin-ajel/tests/rules/ajel-require-error-handling.ts index ed2708a..7b1d961 100644 --- a/packages/eslint-plugin-ajel/tests/rules/ajel-require-error-handling.ts +++ b/packages/eslint-plugin-ajel/tests/rules/ajel-require-error-handling.ts @@ -5,6 +5,7 @@ const tester = new RuleTester(); tester.run('ajel-require-error-handling', rule, { valid: [ + //ajel { code: ` const [res, err] = await ajel(Promise.resolve(1)); @@ -31,6 +32,16 @@ tester.run('ajel-require-error-handling', rule, { } `, }, + { + code: ` + const hello = async () => { + const [res, _err] = await ajel(Promise.resolve(1)); + if (err) { + return _err; + } + } + `, + }, { code: ` const [res, err] = await blimpy(Promise.resolve(1)); @@ -38,10 +49,58 @@ tester.run('ajel-require-error-handling', rule, { return err; } `, - options: [{ ajelAlias: 'blimpy' }], + options: [{ ajelAlias: 'blimpy', sjelAlias: 'sjel' }], + }, + + //sjel + { + code: ` + const [res, err] = sjel(() => null)(); + if (err) { + return err; + } + `, + }, + { + code: ` + const [res, _err] = sjel(() => null)(); + if (err) { + return _err; + } + `, + }, + { + code: ` + async function test() { + const [res, _err] = await sjel(() => null)(); + if (err) { + return _err; + } + } + `, + }, + { + code: ` + const hello = async () => { + const [res, _err] = sjel(() => null)(); + if (err) { + return _err; + } + } + `, + }, + { + code: ` + const [res, err] = limpyb(() => null)(); + if (err) { + return err; + } + `, + options: [{ ajelAlias: 'limpyb', sjelAlias: 'sjel' }], }, ], invalid: [ + //ajel { code: `const [res, _err] = await ajel(Promise.resolve(1)); `, @@ -69,5 +128,46 @@ tester.run('ajel-require-error-handling', rule, { `, errors: [{ messageId: 'requireErrorHandling' }], }, + { + code: `let [res, _err] = await blimpy(Promise.resolve(1)); + `, + options: [{ ajelAlias: 'blimpy', sjelAlias: 'sjel' }], + errors: [{ messageId: 'requireErrorHandling' }], + }, + + //sjel + { + code: `const [res, _err] = sjel(() => null)(); + `, + errors: [{ messageId: 'requireErrorHandling' }], + }, + { + code: ` + async function test() { + const [res, _err] = sjel(() => null)(); + } + `, + errors: [{ messageId: 'requireErrorHandling' }], + }, + { + code: ` + const hello = async () => { + const [_a, _b] = sjel(() => null)(); + console.log(_a); + }; + `, + errors: [{ messageId: 'requireErrorHandling' }], + }, + { + code: `let [res, _err] = sjel(() => null)(); + `, + errors: [{ messageId: 'requireErrorHandling' }], + }, + { + code: `let [res, _err] = limpyb(() => null)(); + `, + options: [{ ajelAlias: 'ajel', sjelAlias: 'limpyb' }], + errors: [{ messageId: 'requireErrorHandling' }], + }, ], }); diff --git a/packages/eslint-plugin-ajel/tests/rules/ajel-require-tuple-declaration.ts b/packages/eslint-plugin-ajel/tests/rules/ajel-require-tuple-declaration.ts index 18a508e..c868060 100644 --- a/packages/eslint-plugin-ajel/tests/rules/ajel-require-tuple-declaration.ts +++ b/packages/eslint-plugin-ajel/tests/rules/ajel-require-tuple-declaration.ts @@ -5,15 +5,26 @@ const tester = new RuleTester(); tester.run('ajel-require-tuple-declaration', rule, { valid: [ + //ajel { code: `const [res, err] = await ajel(Promise.resolve(1));`, }, { code: `const [res, err] = await blimpy(Promise.resolve(1));`, - options: [{ ajelAlias: 'blimpy' }], + options: [{ ajelAlias: 'blimpy', sjelAlias: 'sjel' }], + }, + + //sjel + { + code: `const [res, err] = sjel(Promise.resolve(1));`, + }, + { + code: `const [res, err] = limpyb(Promise.resolve(1));`, + options: [{ ajelAlias: 'ajel', sjelAlias: 'limpyb' }], }, ], invalid: [ + //ajel { code: 'const [err] = await ajel(Promise.resolve(1));', errors: [{ messageId: 'tupleLen2' }], @@ -22,5 +33,15 @@ tester.run('ajel-require-tuple-declaration', rule, { code: 'const [res, err], anothervar = await ajel(Promise.resolve(1));', errors: [{ messageId: 'limitDeclarations' }], }, + + //sjel + { + code: 'const [err] = sjel(() => null)();', + errors: [{ messageId: 'tupleLen2' }], + }, + { + code: 'const [res, err], anothervar = sjel(() => null)();', + errors: [{ messageId: 'limitDeclarations' }], + }, ], }); diff --git a/packages/eslint-plugin-ajel/tests/rules/ajel-strict-error-instanceof.ts b/packages/eslint-plugin-ajel/tests/rules/ajel-strict-error-instanceof.ts index b45665d..8cbdca2 100644 --- a/packages/eslint-plugin-ajel/tests/rules/ajel-strict-error-instanceof.ts +++ b/packages/eslint-plugin-ajel/tests/rules/ajel-strict-error-instanceof.ts @@ -5,6 +5,7 @@ const tester = new RuleTester(); tester.run('ajel-require-error-handling', rule, { valid: [ + //ajel { code: ` const [res, err] = await ajel(Promise.resolve(1)); @@ -20,13 +21,39 @@ tester.run('ajel-require-error-handling', rule, { return err; } `, - options: [{ ajelAlias: 'blimpy' }], + options: [{ ajelAlias: 'blimpy', sjelAlias: 'sjel' }], + }, + + //sjel + { + code: ` + const [res, err] = sjel(() => null)(); + if (err instanceof Error) { + return err; + } + `, + }, + { + code: ` + const [res, err] = limpyb(() => null)(); + if (err instanceof Error) { + return err; + } + `, + options: [{ ajelAlias: 'ajel', sjelAlias: 'limpyb' }], }, ], invalid: [ + //ajel { code: `let [res, err] = await ajel(Promise.resolve(1));`, errors: [{ messageId: 'instanceofError' }], }, + + //sjel + { + code: `let [res, err] = sjel(() => null)();`, + errors: [{ messageId: 'instanceofError' }], + }, ], }); diff --git a/packages/eslint-plugin-ajel/tests/rules/sjel-require-currying.ts b/packages/eslint-plugin-ajel/tests/rules/sjel-require-currying.ts new file mode 100644 index 0000000..1b5528d --- /dev/null +++ b/packages/eslint-plugin-ajel/tests/rules/sjel-require-currying.ts @@ -0,0 +1,31 @@ +import { RuleTester } from '../../node_modules/@typescript-eslint/rule-tester/dist'; +import rule from '../../src/rules/sjel-require-currying'; + +const tester = new RuleTester(); + +tester.run('sjel-require-currying', rule, { + valid: [ + //sjel + { code: `const [res, err] = sjel(() => null)();` }, + { + code: `const [res, err] = limpyb(() => null)();`, + options: [{ sjelAlias: 'limpyb' }], + }, + ], + invalid: [ + //sjel + { + code: ` + const [a, b] = sjel(() => null); + `, + errors: [{ messageId: 'currying' }], + }, + { + code: ` + const [a, b] = limpyb(() => null); + `, + options: [{ sjelAlias: 'limpyb' }], + errors: [{ messageId: 'currying' }], + }, + ], +});