Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions internal/plugins/promise/all.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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,
}
Expand Down
167 changes: 167 additions & 0 deletions internal/plugins/promise/rules/no_nesting/no_nesting.go
Original file line number Diff line number Diff line change
@@ -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 {
Comment thread
swwind marked this conversation as resolved.
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())
}
},
}
},
}
48 changes: 48 additions & 0 deletions internal/plugins/promise/rules/no_nesting/no_nesting.md
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading