no_access_state_in_setstate

package
v0.5.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 14, 2026 License: MIT Imports: 5 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

View Source
var NoAccessStateInSetstateRule = rule.Rule{
	Name: "react/no-access-state-in-setstate",
	Run: func(ctx rule.RuleContext, options any) rule.RuleListeners {
		pragma := reactutil.GetReactPragma(ctx.Settings)
		createClass := reactutil.GetReactCreateClass(ctx.Settings)

		isClassComponent := func(node *ast.Node) bool {
			return reactutil.GetParentReactComponentScopeBased(node, pragma, createClass) != nil
		}

		report := func(node *ast.Node) {
			ctx.ReportNode(node, rule.RuleMessage{
				Id:          "useCallback",
				Description: "Use callback in setState when referencing the previous state.",
			})
		}

		type methodEntry struct {
			methodName string
			node       *ast.Node
		}
		// varEntry stores one tracked binding.
		//
		//   - symbol: non-nil when the binding was resolved through the type
		//     checker. Use-site matching then compares symbol identity, which
		//     correctly distinguishes `let`/`const` bindings declared in
		//     sibling blocks of the same function (ESLint's block-scope
		//     semantics — a strict upstream match).
		//   - scope / variableName: fallback match used when either the
		//     binding or the use site couldn't be resolved to a symbol
		//     (e.g. plain-JS files with no TypeChecker available). Coarser
		//     than block-scope resolution; preserves the previous function-
		//     granular behavior rather than silently under-reporting.
		type varEntry struct {
			node         *ast.Node
			symbol       *ast.Symbol
			scope        *ast.Node
			variableName string
		}
		// methods accumulates function containers whose body reads `this.state`
		// (directly, or transitively via another tracked method). vars
		// accumulates local bindings initialized with / destructured from
		// `this.state` / `this`. Both persist across the whole file walk,
		// matching upstream's closure variables.
		var methods []methodEntry
		var vars []varEntry

		symbolAt := func(node *ast.Node) *ast.Symbol {
			if ctx.TypeChecker == nil || node == nil {
				return nil
			}
			return ctx.TypeChecker.GetSymbolAtLocation(node)
		}

		return rule.RuleListeners{
			ast.KindPropertyAccessExpression: func(node *ast.Node) {
				if !isThisStateMember(node) {
					return
				}
				if !isClassComponent(node) {
					return
				}
				for current := node; current != nil; current = current.Parent {
					if isFirstArgumentInSetStateCall(current, node) {
						report(node)
						return
					}
					if kind := methodContainerKind(current); kind != containerNone {
						methods = append(methods, methodEntry{
							methodName: containerKeyName(current, kind),
							node:       node,
						})
						return
					}
					if current.Kind == ast.KindVariableDeclaration {
						declName := current.AsVariableDeclaration().Name()
						vars = append(vars, varEntry{
							node:         node,
							symbol:       symbolAt(declName),
							scope:        utils.FindEnclosingScope(node),
							variableName: identifierOrEmpty(declName),
						})
						return
					}
				}
			},

			ast.KindCallExpression: func(node *ast.Node) {
				if !isClassComponent(node) {
					return
				}
				call := node.AsCallExpression()

				calleeName := ""
				switch call.Expression.Kind {
				case ast.KindIdentifier:
					calleeName = call.Expression.AsIdentifier().Text
				case ast.KindPrivateIdentifier:
					calleeName = strings.TrimPrefix(call.Expression.AsPrivateIdentifier().Text, "#")
				}

				if calleeName != "" {

					n := len(methods)
					for i := range n {
						method := methods[i]
						if method.methodName != calleeName {
							continue
						}
						if enclosing := findEnclosingClassMethod(node.Parent); enclosing != nil {
							methods = append(methods, methodEntry{
								methodName: containerKeyName(enclosing, containerClass),
								node:       method.node,
							})
						}
					}
				}

				for current := node.Parent; current != nil; current = current.Parent {
					if !isFirstArgumentInSetStateCall(current, node) {
						continue
					}
					if calleeName != "" {
						for _, method := range methods {
							if method.methodName == calleeName {
								report(method.node)
							}
						}
					}
					return
				}
			},

			ast.KindIdentifier: func(node *ast.Node) {
				if !isValueOrObjectPosition(node) {
					return
				}

				start := walkUpBinary(node)
				useName := node.AsIdentifier().Text

				useSymbol := utils.GetReferenceSymbol(node, ctx.TypeChecker)
				var useScope *ast.Node // lazily computed for fallback
				for current := start; current != nil; current = current.Parent {
					if !isFirstArgumentInSetStateCall(current, node) {
						continue
					}
					for _, v := range vars {
						var matched bool
						if v.symbol != nil && useSymbol != nil {
							matched = v.symbol == useSymbol
						} else {
							if useScope == nil {
								useScope = utils.FindEnclosingScope(node)
							}
							matched = v.scope == useScope && v.variableName == useName
						}
						if matched {
							report(v.node)
						}
					}

				}
			},

			ast.KindObjectBindingPattern: func(node *ast.Node) {

				parent := node.Parent
				if parent == nil || parent.Kind != ast.KindVariableDeclaration {
					return
				}
				init := parent.AsVariableDeclaration().Initializer
				if init == nil || !isThisReceiver(init) {
					return
				}
				scope := utils.FindEnclosingScope(node)
				pat := node.AsBindingPattern()
				if pat == nil || pat.Elements == nil {
					return
				}
				for _, elem := range pat.Elements.Nodes {
					if elem.Kind != ast.KindBindingElement {
						continue
					}
					if objectBindingPatternPropertyName(elem) != "state" {
						continue
					}
					be := elem.AsBindingElement()

					keyNode := be.PropertyName
					if keyNode == nil {
						keyNode = be.Name()
					}
					// Only capture a symbol for shorthand destructuring.
					// Renamed form `{state: aliased}` deliberately goes through
					// the name-based fallback: upstream's variableName='state'
					// will never match the user-typed 'aliased' at the use
					// site, so binding the local 'aliased' symbol here would
					// divergently "fix" a latent upstream quirk. Keeping the
					// renamed path symbol-less preserves upstream parity.
					var sym *ast.Symbol
					if be.PropertyName == nil {
						sym = symbolAt(be.Name())
					}
					vars = append(vars, varEntry{
						node:         keyNode,
						symbol:       sym,
						scope:        scope,
						variableName: "state",
					})
				}
			},
		}
	},
}

Functions

This section is empty.

Types

This section is empty.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL