diff --git a/internal/plugins/promise/all.go b/internal/plugins/promise/all.go index 549c1eca0..58355a5da 100644 --- a/internal/plugins/promise/all.go +++ b/internal/plugins/promise/all.go @@ -5,6 +5,7 @@ import ( "github.com/web-infra-dev/rslint/internal/plugins/promise/rules/avoid_new" "github.com/web-infra-dev/rslint/internal/plugins/promise/rules/catch_or_return" "github.com/web-infra-dev/rslint/internal/plugins/promise/rules/no_multiple_resolved" + "github.com/web-infra-dev/rslint/internal/plugins/promise/rules/no_nesting" "github.com/web-infra-dev/rslint/internal/plugins/promise/rules/no_return_wrap" "github.com/web-infra-dev/rslint/internal/plugins/promise/rules/param_names" "github.com/web-infra-dev/rslint/internal/rule" @@ -16,6 +17,7 @@ func GetAllRules() []rule.Rule { avoid_new.AvoidNewRule, catch_or_return.CatchOrReturnRule, no_multiple_resolved.NoMultipleResolvedRule, + no_nesting.NoNestingRule, no_return_wrap.NoReturnWrapRule, param_names.ParamNamesRule, } diff --git a/internal/plugins/promise/rules/no_nesting/no_nesting.go b/internal/plugins/promise/rules/no_nesting/no_nesting.go new file mode 100644 index 000000000..9fc45c5de --- /dev/null +++ b/internal/plugins/promise/rules/no_nesting/no_nesting.go @@ -0,0 +1,167 @@ +package no_nesting + +import ( + "github.com/microsoft/typescript-go/shim/ast" + "github.com/web-infra-dev/rslint/internal/plugins/promise/promiseutil" + "github.com/web-infra-dev/rslint/internal/rule" + "github.com/web-infra-dev/rslint/internal/utils" +) + +const skipTransparent = ast.OEKParentheses + +// isThenOrCatchCall reports whether node is a call whose callee is a non-computed +// .then or .catch member access. Mirrors eslint-plugin-promise's has-promise-callback. +func isThenOrCatchCall(node *ast.Node) bool { + return promiseutil.IsMemberCall(node, "then") || promiseutil.IsMemberCall(node, "catch") +} + +// isPromiseCallback reports whether node is a FunctionExpression or ArrowFunction +// directly passed as argument to a .then() or .catch() call. +// Mirrors eslint-plugin-promise's lib/is-inside-promise helper. +func isPromiseCallback(node *ast.Node) bool { + if node.Kind != ast.KindFunctionExpression && node.Kind != ast.KindArrowFunction { + return false + } + // In tsgo, an argument node's parent is the containing CallExpression. + parent := node.Parent + for parent != nil && ast.IsOuterExpression(parent, skipTransparent) { + parent = parent.Parent + } + return isThenOrCatchCall(parent) +} + +// walkIdentifiers calls fn for each identifier node found anywhere in node's +// subtree, stopping early across the whole traversal if fn returns true. +func walkIdentifiers(node *ast.Node, fn func(identNode *ast.Node) bool) bool { + if node == nil { + return false + } + if node.Kind == ast.KindIdentifier { + return fn(node) + } + found := false + node.ForEachChild(func(child *ast.Node) bool { + if walkIdentifiers(child, fn) { + found = true + return true + } + return false + }) + return found +} + +// argsContainRef reports whether any identifier anywhere in the call's argument +// list has a name that is bound in fn's parameter list or local variable +// declarations. Uses utils.HasShadowingParameter for params and +// utils.HasShadowingDeclaration / utils.HasHoistedVarDeclaration for body +// declarations, mirroring the upstream's scope-reference check. +func argsContainRef(callNode *ast.Node, fn *ast.Node) bool { + args := callNode.AsCallExpression().Arguments + if args == nil { + return false + } + body := fn.Body() + boundary := fn + if body != nil { + boundary = body + } + for _, arg := range args.Nodes { + found := false + walkIdentifiers(arg, func(identNode *ast.Node) bool { + if utils.IsNonReferenceIdentifier(identNode) { + return false + } + + name := identNode.AsIdentifier().Text + + if name == "arguments" { + if utils.IsNameShadowedBetween(identNode, boundary, name) { + return false + } + isArgumentsShadowed := false + for curr := identNode.Parent; curr != nil && curr != fn; curr = curr.Parent { + if ast.IsFunctionLikeDeclaration(curr) && curr.Kind != ast.KindArrowFunction { + isArgumentsShadowed = true + break + } + } + if isArgumentsShadowed { + return false + } + if fn.Kind != ast.KindArrowFunction { + found = true + return true + } + return false + } + + hasParam := utils.HasShadowingParameter(fn, name) + hasDecl := body != nil && (utils.HasShadowingDeclaration(body, name) || utils.HasHoistedVarDeclaration(body, name)) + if !hasParam && !hasDecl { + return false + } + + if utils.IsNameShadowedBetween(identNode, boundary, name) { + return false + } + + found = true + return true + }) + if found { + return true + } + } + return false +} + +func buildAvoidNestingMessage() rule.RuleMessage { + return rule.RuleMessage{ + Id: "avoidNesting", + Description: "Avoid nesting promises.", + } +} + +var NoNestingRule = rule.Rule{ + Name: "promise/no-nesting", + Run: func(ctx rule.RuleContext, options any) rule.RuleListeners { + // Stack of promise-callback function nodes, closest last. + callbackStack := []*ast.Node{} + + onEnter := func(node *ast.Node) { + if isPromiseCallback(node) { + callbackStack = append(callbackStack, node) + } + } + onExit := func(node *ast.Node) { + if isPromiseCallback(node) && len(callbackStack) > 0 { + callbackStack = callbackStack[:len(callbackStack)-1] + } + } + + return rule.RuleListeners{ + ast.KindFunctionExpression: onEnter, + ast.KindArrowFunction: onEnter, + rule.ListenerOnExit(ast.KindFunctionExpression): onExit, + rule.ListenerOnExit(ast.KindArrowFunction): onExit, + + ast.KindCallExpression: func(node *ast.Node) { + if !isThenOrCatchCall(node) || len(callbackStack) == 0 { + return + } + closestCallback := callbackStack[len(callbackStack)-1] + if argsContainRef(node, closestCallback) { + return + } + callee := ast.SkipOuterExpressions(node.AsCallExpression().Expression, skipTransparent) + if callee == nil || !ast.IsPropertyAccessExpression(callee) { + return + } + nameNode := callee.AsPropertyAccessExpression().Name() + if nameNode != nil { + ctx.ReportNode(nameNode, buildAvoidNestingMessage()) + } + }, + } + }, +} diff --git a/internal/plugins/promise/rules/no_nesting/no_nesting.md b/internal/plugins/promise/rules/no_nesting/no_nesting.md new file mode 100644 index 000000000..6118f0e1f --- /dev/null +++ b/internal/plugins/promise/rules/no_nesting/no_nesting.md @@ -0,0 +1,48 @@ +# promise/no-nesting + +## Rule Details + +Disallow nesting `.then()` or `.catch()` statements inside promise callbacks when the +inner call does not depend on variables introduced by the enclosing callback. + +Deeply nested promise chains are harder to read and can usually be rewritten as a flat +chain. This rule flags the inner `.then()` / `.catch()` when its arguments do not +reference any binding (parameter or local variable) that belongs to the immediately +enclosing promise callback, because in that case the nesting is unnecessary. + +Examples of **incorrect** code for this rule: + +```javascript +doThing().then(function () { + return a.then(); +}); + +doThing().then(() => b.catch()); + +doThing().then(function () { + return a.then(function () { + return b.catch(); + }); +}); +``` + +Examples of **correct** code for this rule: + +```javascript +// Flat chain — no nesting +doThing().then(function () { + return 4; +}); + +// Inner call uses a closure variable from the enclosing callback — cannot be flattened +doThing().then((a) => getB(a).then((b) => getC(a, b))); + +// Promise.resolve / Promise.all inside a callback is fine +doThing().then(function () { + return Promise.resolve(4); +}); +``` + +## Original Documentation + +https://github.com/eslint-community/eslint-plugin-promise/blob/main/docs/rules/no-nesting.md diff --git a/internal/plugins/promise/rules/no_nesting/no_nesting_extras_test.go b/internal/plugins/promise/rules/no_nesting/no_nesting_extras_test.go new file mode 100644 index 000000000..e2ddb10ee --- /dev/null +++ b/internal/plugins/promise/rules/no_nesting/no_nesting_extras_test.go @@ -0,0 +1,229 @@ +// TestNoNestingExtras locks in branches and edge shapes that the upstream test suite +// doesn't exercise. Each case carries an inline comment pointing at the specific +// branch / Dimension 4 row / tsgo AST quirk it covers, so future refactors can't +// silently regress them without breaking a named lock-in. + +package no_nesting_test + +import ( + "testing" + + "github.com/web-infra-dev/rslint/internal/plugins/promise/fixtures" + "github.com/web-infra-dev/rslint/internal/plugins/promise/rules/no_nesting" + "github.com/web-infra-dev/rslint/internal/rule_tester" +) + +func TestNoNestingExtras(t *testing.T) { + rule_tester.RunRuleTester( + fixtures.GetRootDir(), + "tsconfig.json", + t, + &no_nesting.NoNestingRule, + []rule_tester.ValidTestCase{ + // ---- Dimension 4: parenthesized receiver on outer .then call ---- + // The receiver of .then is parenthesized; the function IS still detected + // as a promise callback, and the inner .then is exempt because it uses `a`. + {Code: `(doThing()).then(a => innerThing().then(b => getC(a, b)))`}, + + // ---- Dimension 4: element access ['then'] is not detected ---- + // IsMemberCall requires a PropertyAccessExpression; computed access is ignored + // so the function is NOT pushed to the callback stack. + {Code: `doThing()['then'](function() { a.then() })`}, + + // ---- Dimension 4: optional-chain on outer .then call ---- + // IsMemberCall checks the property name regardless of QuestionDotToken. + // The function is pushed; its inner .then uses `a` from the outer scope → skip. + {Code: `doThing()?.then(a => innerThing().then(b => getC(a, b)))`}, + + // ---- Dimension 4: parenthesized callback function ---- + // isPromiseCallback skips outer expressions (parentheses), so (fn) still + // detects the outer function as a promise callback. Inner .then uses `a` → skip. + {Code: `doThing().then((a => innerThing().then(b => getC(a, b))))`}, + + // ---- Dimension 4: async arrow function callback ---- + // Async modifier doesn't affect isThenOrCatchCall or isPromiseCallback. + {Code: `doThing().then(async a => innerThing().then(b => getC(a, b)))`}, + + // ---- Dimension 4: local const — closure from const declaration ---- + // A local `const` inside the callback body is collected by collectScopeBindings; + // the inner .then references it → skip. + {Code: `doThing().then(a => { + const result = doInner(a); + return result.then(b => getC(result, b)); +})`}, + + // ---- Dimension 4: local var inside if-block — collected across blocks ---- + {Code: `doThing().then(function() { + var x = 1; + if (cond) { var y = 2; } + return inner().then(b => getC(x, b)); +})`}, + + // ---- Dimension 4: local var in for-loop initializer is collected ---- + // collectDeclsInStmt handles KindVariableDeclarationList nodes which appear + // as for-loop initializers. + {Code: `doThing().then(function() { + for (var i = 0; i < 10; i++) {} + return inner().then(b => done(i, b)); +})`}, + + // ---- Dimension 4: flat chain — second .then callback has no nested call ---- + // The second callback `() => b` contains only an identifier reference, not a + // .then/.catch call, so there is nothing to report. + {Code: `doThing().then(() => a).then(() => b)`}, + + // ---- Dimension 4: non-arrow, non-function argument — no push ---- + // A variable reference passed to .then is invisible to isPromiseCallback. + {Code: `doThing().then(handler)`}, + + // ---- Dimension 4: .then with no arguments — no callback pushed ---- + {Code: `doThing().then()`}, + + // ---- Real-user: promise returned directly without nesting ---- + {Code: `doThing().then(() => otherThing())`}, + + // ---- Dimension 4: destructured params are collected ---- + // CollectBindingNames recurses into ObjectBindingPattern; `x` from the + // destructured param is found in the inner .then's arg → skip. + {Code: `doThing().then(({ x }) => inner(x).then(b => done(x, b)))`}, + + // ---- Dimension 4: rest parameter is collected ---- + {Code: `doThing().then((...args) => inner(args).then(b => done(args, b)))`}, + + // ---- Dimension 4: function declaration inside callback uses closure var ---- + // FunctionDeclaration is NOT pushed to the callback stack (isPromiseCallback + // only fires for FunctionExpression/ArrowFunction). The outer callback has `x` + // as a param; `a.then(x)` inside the FunctionDeclaration references `x` → skip. + {Code: `doThing().then(function(x) { + function inner() { return a.then(x) } + return inner(); +})`}, + + // ---- Dimension 4: getter inside callback uses closure var ---- + // Same reasoning: class methods / accessors are not pushed to the stack. + {Code: `doThing().then(function(x) { + return { get val() { return a.then(x) } }; +})`}, + + // ---- Dimension 4: implicit arguments binding in non-arrow callback ---- + {Code: `doThing().then(function() { return foo.then(() => use(arguments)) })`}, + }, + []rule_tester.InvalidTestCase{ + // ---- Dimension 4: parenthesized callback — still reports inner .then ---- + // The outer function is correctly detected (parens are skipped). Inner .then + // has no args referencing the outer scope → report. + { + Code: `doThing().then((function() { a.then() }))`, + Errors: []rule_tester.InvalidTestCaseError{{MessageId: "avoidNesting", Line: 1}}, + }, + + // ---- Dimension 4: optional-chain outer call — inner .then reported ---- + { + Code: `doThing()?.then(function() { a.then() })`, + Errors: []rule_tester.InvalidTestCaseError{{MessageId: "avoidNesting", Line: 1}}, + }, + + // ---- Dimension 4: async function callback ---- + { + Code: `doThing().then(async function() { a.then() })`, + Errors: []rule_tester.InvalidTestCaseError{{MessageId: "avoidNesting", Line: 1}}, + }, + + // ---- Dimension 4: named function expression callback ---- + { + Code: `doThing().then(function named() { a.then() })`, + Errors: []rule_tester.InvalidTestCaseError{{MessageId: "avoidNesting", Line: 1}}, + }, + + // ---- Dimension 4: catch as outer callback ---- + { + Code: `doThing().catch(function() { a.then() })`, + Errors: []rule_tester.InvalidTestCaseError{{MessageId: "avoidNesting", Line: 1}}, + }, + + // ---- Dimension 4: function declaration inside callback — no closure var used ---- + // The FunctionDeclaration is not pushed to the stack, but the outer callback IS + // on the stack. a.then() inside the declaration has no args referencing the outer + // scope → report. This matches ESLint's behavior. + { + Code: `doThing().then(function() { + function inner() { return a.then() } + return inner(); +})`, + Errors: []rule_tester.InvalidTestCaseError{{MessageId: "avoidNesting", Line: 2}}, + }, + + // ---- Dimension 4: getter inside callback — no closure var used ---- + // The accessor method is not pushed to the stack; a.then() with no closure + // refs → report. + { + Code: `doThing().then(function() { + return { get x() { return a.then() } }; +})`, + Errors: []rule_tester.InvalidTestCaseError{{MessageId: "avoidNesting", Line: 2}}, + }, + + // ---- Dimension 4: chained .then callback itself contains a nested call ---- + // The second .then's callback `() => b.catch()` has b.catch() inside it. + // That b.catch() is nested in the second callback scope with no closure refs → report. + { + Code: `doThing().then(() => a).then(() => b.catch())`, + Errors: []rule_tester.InvalidTestCaseError{{MessageId: "avoidNesting", Line: 1}}, + }, + + // ---- Dimension 4: 3-level nesting reports both foldable levels ---- + // Both a.then() and b.then() are independently reportable: + // a.then(b=>..) → outer callback has `a` but inner arg has no `a` usage → report + // b.then(c=>c) → middle callback has `b` but inner arg has no `b` → report + { + Code: `doThing().then(a => a.then(b => b.then(c => c)))`, + Errors: []rule_tester.InvalidTestCaseError{ + {MessageId: "avoidNesting", Line: 1}, + {MessageId: "avoidNesting", Line: 1}, + }, + }, + + // Locks in upstream arm: callbackScopes non-empty and inner args don't ref closure. + { + Code: `doThing().then(() => a.catch())`, + Errors: []rule_tester.InvalidTestCaseError{{MessageId: "avoidNesting", Line: 1}}, + }, + + // ---- Real-user: rejection handler (second arg) contains nested .then ---- + { + Code: `doThing().then(null, function() { a.then() })`, + Errors: []rule_tester.InvalidTestCaseError{{MessageId: "avoidNesting", Line: 1}}, + }, + + // ---- Real-user: .catch rejection handler contains nested .catch ---- + { + Code: `doThing().catch(function() { a.catch() })`, + Errors: []rule_tester.InvalidTestCaseError{{MessageId: "avoidNesting", Line: 1}}, + }, + + // ---- Dimension 4: property access name is not a reference to outer binding ---- + { + Code: `doThing().then(user => getProfile().then(p => p.user))`, + Errors: []rule_tester.InvalidTestCaseError{{MessageId: "avoidNesting", Line: 1}}, + }, + + // ---- Dimension 4: object key is not a reference to outer binding ---- + { + Code: `doThing().then(user => getProfile().then(p => ({ user: 1 })))`, + Errors: []rule_tester.InvalidTestCaseError{{MessageId: "avoidNesting", Line: 1}}, + }, + + // ---- Dimension 4: inner parameter shadowing the outer binding ---- + { + Code: `doThing().then(a => inner.then(a => use(a)))`, + Errors: []rule_tester.InvalidTestCaseError{{MessageId: "avoidNesting", Line: 1}}, + }, + + // ---- Dimension 4: implicit arguments binding shadowed by nested non-arrow callback ---- + { + Code: `doThing().then(function() { return foo.then(function() { use(arguments) }) })`, + Errors: []rule_tester.InvalidTestCaseError{{MessageId: "avoidNesting", Line: 1}}, + }, + }, + ) +} diff --git a/internal/plugins/promise/rules/no_nesting/no_nesting_upstream_test.go b/internal/plugins/promise/rules/no_nesting/no_nesting_upstream_test.go new file mode 100644 index 000000000..d1193a677 --- /dev/null +++ b/internal/plugins/promise/rules/no_nesting/no_nesting_upstream_test.go @@ -0,0 +1,123 @@ +// TestNoNestingUpstream migrates the full valid/invalid suite from upstream +// __tests__/no-nesting.js 1:1. Position assertions cover line/column for every +// invalid case. rslint-specific lock-in cases live in no_nesting_extras_test.go. + +package no_nesting_test + +import ( + "testing" + + "github.com/web-infra-dev/rslint/internal/plugins/promise/fixtures" + "github.com/web-infra-dev/rslint/internal/plugins/promise/rules/no_nesting" + "github.com/web-infra-dev/rslint/internal/rule_tester" +) + +const avoidNestingMessage = "Avoid nesting promises." + +func TestNoNestingUpstream(t *testing.T) { + rule_tester.RunRuleTester( + fixtures.GetRootDir(), + "tsconfig.json", + t, + &no_nesting.NoNestingRule, + []rule_tester.ValidTestCase{ + // ---- resolve and reject are sometimes okay ---- + {Code: `Promise.resolve(4).then(function(x) { return x })`}, + {Code: `Promise.reject(4).then(function(x) { return x })`}, + {Code: `Promise.resolve(4).then(function() {})`}, + {Code: `Promise.reject(4).then(function() {})`}, + + // ---- throw and return are fine ---- + {Code: `doThing().then(function() { return 4 })`}, + {Code: `doThing().then(function() { throw 4 })`}, + {Code: `doThing().then(null, function() { return 4 })`}, + {Code: `doThing().then(null, function() { throw 4 })`}, + {Code: `doThing().catch(null, function() { return 4 })`}, + {Code: `doThing().catch(null, function() { throw 4 })`}, + + // ---- arrow functions and other things ---- + {Code: `doThing().then(() => 4)`}, + {Code: `doThing().then(() => { throw 4 })`}, + {Code: `doThing().then(()=>{}, () => 4)`}, + {Code: `doThing().then(()=>{}, () => { throw 4 })`}, + {Code: `doThing().catch(() => 4)`}, + {Code: `doThing().catch(() => { throw 4 })`}, + + // ---- random functions and callback methods ---- + {Code: `var x = function() { return Promise.resolve(4) }`}, + {Code: `function y() { return Promise.resolve(4) }`}, + {Code: `function then() { return Promise.reject() }`}, + {Code: `doThing(function(x) { return Promise.reject(x) })`}, + + // ---- Promise statics and Promise.all are fine inside callbacks ---- + {Code: `doThing().then(function() { return Promise.all([a,b,c]) })`}, + {Code: `doThing().then(function() { return Promise.resolve(4) })`}, + {Code: `doThing().then(() => Promise.resolve(4))`}, + {Code: `doThing().then(() => Promise.all([a]))`}, + + // ---- references vars in closure ---- + {Code: `doThing() + .then(a => getB(a) + .then(b => getC(a, b)) + )`}, + {Code: `doThing() + .then(a => { + const c = a * 2; + return getB(c).then(b => getC(c, b)) + })`}, + }, + []rule_tester.InvalidTestCase{ + { + Code: `doThing().then(function() { a.then() })`, + Errors: []rule_tester.InvalidTestCaseError{{MessageId: "avoidNesting", Message: avoidNestingMessage, Line: 1, Column: 31}}, + }, + { + Code: `doThing().then(function() { b.catch() })`, + Errors: []rule_tester.InvalidTestCaseError{{MessageId: "avoidNesting", Message: avoidNestingMessage, Line: 1, Column: 31}}, + }, + { + Code: `doThing().then(function() { return a.then() })`, + Errors: []rule_tester.InvalidTestCaseError{{MessageId: "avoidNesting", Message: avoidNestingMessage, Line: 1, Column: 38}}, + }, + { + Code: `doThing().then(function() { return b.catch() })`, + Errors: []rule_tester.InvalidTestCaseError{{MessageId: "avoidNesting", Message: avoidNestingMessage, Line: 1, Column: 38}}, + }, + { + Code: `doThing().then(() => { a.then() })`, + Errors: []rule_tester.InvalidTestCaseError{{MessageId: "avoidNesting", Message: avoidNestingMessage, Line: 1, Column: 26}}, + }, + { + Code: `doThing().then(() => { b.catch() })`, + Errors: []rule_tester.InvalidTestCaseError{{MessageId: "avoidNesting", Message: avoidNestingMessage, Line: 1, Column: 26}}, + }, + { + Code: `doThing().then(() => a.then())`, + Errors: []rule_tester.InvalidTestCaseError{{MessageId: "avoidNesting", Message: avoidNestingMessage, Line: 1, Column: 24}}, + }, + { + Code: `doThing().then(() => b.catch())`, + Errors: []rule_tester.InvalidTestCaseError{{MessageId: "avoidNesting", Message: avoidNestingMessage, Line: 1, Column: 24}}, + }, + // ---- references vars in closure ---- + { + Code: ` + doThing() + .then(a => getB(a) + .then(b => getC(b)) + )`, + Errors: []rule_tester.InvalidTestCaseError{{MessageId: "avoidNesting", Message: avoidNestingMessage, Line: 4}}, + }, + { + Code: ` + doThing() + .then(a => getB(a) + .then(b => getC(a, b) + .then(c => getD(a, c)) + ) + )`, + Errors: []rule_tester.InvalidTestCaseError{{MessageId: "avoidNesting", Message: avoidNestingMessage, Line: 5}}, + }, + }, + ) +} diff --git a/packages/rslint-test-tools/rstest.config.mts b/packages/rslint-test-tools/rstest.config.mts index 8466baf09..809e2329d 100644 --- a/packages/rslint-test-tools/rstest.config.mts +++ b/packages/rslint-test-tools/rstest.config.mts @@ -473,6 +473,7 @@ export default defineConfig({ './tests/eslint-plugin-promise/rules/avoid-new.test.ts', './tests/eslint-plugin-promise/rules/catch-or-return.test.ts', './tests/eslint-plugin-promise/rules/no-multiple-resolved.test.ts', + './tests/eslint-plugin-promise/rules/no-nesting.test.ts', './tests/eslint-plugin-promise/rules/no-return-wrap.test.ts', './tests/eslint-plugin-promise/rules/param-names.test.ts', diff --git a/packages/rslint-test-tools/tests/eslint-plugin-promise/rules/no-nesting.test.ts b/packages/rslint-test-tools/tests/eslint-plugin-promise/rules/no-nesting.test.ts new file mode 100644 index 000000000..66a820fff --- /dev/null +++ b/packages/rslint-test-tools/tests/eslint-plugin-promise/rules/no-nesting.test.ts @@ -0,0 +1,106 @@ +import { RuleTester } from '../rule-tester'; + +const ruleTester = new RuleTester(); + +const avoidNestingMessage = 'Avoid nesting promises.'; + +ruleTester.run('no-nesting', {} as never, { + valid: [ + // resolve and reject are sometimes okay + { code: 'Promise.resolve(4).then(function(x) { return x })' }, + { code: 'Promise.reject(4).then(function(x) { return x })' }, + { code: 'Promise.resolve(4).then(function() {})' }, + { code: 'Promise.reject(4).then(function() {})' }, + + // throw and return are fine + { code: 'doThing().then(function() { return 4 })' }, + { code: 'doThing().then(function() { throw 4 })' }, + { code: 'doThing().then(null, function() { return 4 })' }, + { code: 'doThing().then(null, function() { throw 4 })' }, + { code: 'doThing().catch(null, function() { return 4 })' }, + { code: 'doThing().catch(null, function() { throw 4 })' }, + + // arrow functions and other things + { code: 'doThing().then(() => 4)' }, + { code: 'doThing().then(() => { throw 4 })' }, + { code: 'doThing().then(()=>{}, () => 4)' }, + { code: 'doThing().then(()=>{}, () => { throw 4 })' }, + { code: 'doThing().catch(() => 4)' }, + { code: 'doThing().catch(() => { throw 4 })' }, + + // random functions and callback methods + { code: 'var x = function() { return Promise.resolve(4) }' }, + { code: 'function y() { return Promise.resolve(4) }' }, + { code: 'function then() { return Promise.reject() }' }, + { code: 'doThing(function(x) { return Promise.reject(x) })' }, + + // Promise statics and Promise.all are fine inside callbacks + { code: 'doThing().then(function() { return Promise.all([a,b,c]) })' }, + { code: 'doThing().then(function() { return Promise.resolve(4) })' }, + { code: 'doThing().then(() => Promise.resolve(4))' }, + { code: 'doThing().then(() => Promise.all([a]))' }, + + // references vars in closure + { code: 'doThing().then(a => getB(a).then(b => getC(a, b)))' }, + { + code: `doThing().then(a => { + const c = a * 2; + return getB(c).then(b => getC(c, b)) + })`, + }, + ], + + invalid: [ + { + code: 'doThing().then(function() { a.then() })', + errors: [{ message: avoidNestingMessage }], + }, + { + code: 'doThing().then(function() { b.catch() })', + errors: [{ message: avoidNestingMessage }], + }, + { + code: 'doThing().then(function() { return a.then() })', + errors: [{ message: avoidNestingMessage }], + }, + { + code: 'doThing().then(function() { return b.catch() })', + errors: [{ message: avoidNestingMessage }], + }, + { + code: 'doThing().then(() => { a.then() })', + errors: [{ message: avoidNestingMessage }], + }, + { + code: 'doThing().then(() => { b.catch() })', + errors: [{ message: avoidNestingMessage }], + }, + { + code: 'doThing().then(() => a.then())', + errors: [{ message: avoidNestingMessage }], + }, + { + code: 'doThing().then(() => b.catch())', + errors: [{ message: avoidNestingMessage }], + }, + // references vars in closure + { + code: ` + doThing() + .then(a => getB(a) + .then(b => getC(b)) + )`, + errors: [{ message: avoidNestingMessage }], + }, + { + code: ` + doThing() + .then(a => getB(a) + .then(b => getC(a, b) + .then(c => getD(a, c)) + ) + )`, + errors: [{ message: avoidNestingMessage }], + }, + ], +});