no_array_index_key

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: 3 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

View Source
var NoArrayIndexKeyRule = rule.Rule{
	Name: "react/no-array-index-key",
	Run: func(ctx rule.RuleContext, options any) rule.RuleListeners {
		pragma := reactutil.GetReactPragma(ctx.Settings)

		// indexParamNames is a stack of identifier names introduced as the
		// index parameter of an enclosing tracked iterator callback. An
		// identifier reference whose text matches any name on the stack is
		// treated as the array index. Mirrors upstream's flat array — pushed
		// on iterator-call enter, popped on exit. The stack is intentionally
		// a flat name list (not a depth counter): nested iterators with the
		// SAME index name (`foo.map((a,i)=>bar.map((c,i)=>...))`) push the
		// name twice, and inner functions defined inside the callback do
		// NOT shadow it — both behaviors mirror upstream byte-for-byte.
		var indexParamNames []string

		isArrayIndex := func(node *ast.Node) bool {
			if node == nil {
				return false
			}
			node = ast.SkipParentheses(node)
			if node == nil || node.Kind != ast.KindIdentifier {
				return false
			}
			name := node.AsIdentifier().Text
			for _, n := range indexParamNames {
				if n == name {
					return true
				}
			}
			return false
		}

		isUsingReactChildren := func(call *ast.CallExpression) bool {
			if call == nil || call.Expression == nil {
				return false
			}
			callee := ast.SkipParentheses(call.Expression)
			if callee == nil || callee.Kind != ast.KindPropertyAccessExpression {
				return false
			}
			pa := callee.AsPropertyAccessExpression()
			name := pa.Name()
			if name == nil || name.Kind != ast.KindIdentifier {
				return false
			}
			method := name.AsIdentifier().Text
			if method != "map" && method != "forEach" {
				return false
			}
			obj := ast.SkipParentheses(pa.Expression)
			if obj == nil {
				return false
			}
			if obj.Kind == ast.KindIdentifier && obj.AsIdentifier().Text == "Children" {
				return true
			}
			if obj.Kind == ast.KindPropertyAccessExpression {
				inner := obj.AsPropertyAccessExpression()
				innerObj := ast.SkipParentheses(inner.Expression)
				return innerObj != nil && innerObj.Kind == ast.KindIdentifier && innerObj.AsIdentifier().Text == pragma
			}
			return false
		}

		getMapIndexParamName := func(call *ast.CallExpression) string {
			if call == nil || call.Expression == nil {
				return ""
			}
			callee := ast.SkipParentheses(call.Expression)
			if callee == nil || callee.Kind != ast.KindPropertyAccessExpression {
				return ""
			}
			pa := callee.AsPropertyAccessExpression()
			name := pa.Name()
			if name == nil || name.Kind != ast.KindIdentifier {
				return ""
			}
			pos, ok := indexParamPositions[name.AsIdentifier().Text]
			if !ok {
				return ""
			}
			if call.Arguments == nil {
				return ""
			}
			args := call.Arguments.Nodes
			argIdx := 0
			if isUsingReactChildren(call) {
				argIdx = 1
			}
			if argIdx >= len(args) {
				return ""
			}
			callback := ast.SkipParentheses(args[argIdx])
			if callback == nil || !ast.IsFunctionExpressionOrArrowFunction(callback) {
				return ""
			}
			params := callback.Parameters()
			if pos >= len(params) {
				return ""
			}
			param := params[pos]
			if param == nil || param.Kind != ast.KindParameter {
				return ""
			}
			pd := param.AsParameterDeclaration()

			if pd.DotDotDotToken != nil {
				return ""
			}

			if pd.Initializer != nil {
				return ""
			}
			paramName := pd.Name()

			if paramName == nil || paramName.Kind != ast.KindIdentifier {
				return ""
			}
			return paramName.AsIdentifier().Text
		}

		report := func(node *ast.Node) {
			ctx.ReportNode(node, rule.RuleMessage{
				Id:          "noArrayIndex",
				Description: noArrayIndexMessage,
			})
		}

		// collectIdentifiersFromBinary recursively flattens a chain of
		// BinaryExpressions into the bare identifiers they reference.
		// Mirrors upstream's `getIdentifiersFromBinaryExpression`: only
		// Identifier and BinaryExpression are walked — every other shape
		// (ConditionalExpression, CallExpression, MemberExpression, …)
		// is opaque, matching upstream's selective recursion.
		var collectIdentifiersFromBinary func(node *ast.Node) []*ast.Node
		collectIdentifiersFromBinary = func(node *ast.Node) []*ast.Node {
			if node == nil {
				return nil
			}
			node = ast.SkipParentheses(node)
			if node == nil {
				return nil
			}
			if node.Kind == ast.KindIdentifier {
				return []*ast.Node{node}
			}
			if node.Kind == ast.KindBinaryExpression {
				be := node.AsBinaryExpression()
				return append(collectIdentifiersFromBinary(be.Left), collectIdentifiersFromBinary(be.Right)...)
			}
			return nil
		}

		checkPropValue := func(node *ast.Node) {
			if node == nil {
				return
			}
			node = ast.SkipParentheses(node)
			if node == nil {
				return
			}

			if isArrayIndex(node) {
				report(node)
				return
			}

			if node.Kind == ast.KindTemplateExpression {
				te := node.AsTemplateExpression()
				if te.TemplateSpans != nil {
					for _, span := range te.TemplateSpans.Nodes {
						if span.Kind != ast.KindTemplateSpan {
							continue
						}
						if isArrayIndex(span.AsTemplateSpan().Expression) {
							report(node)
						}
					}
				}
				return
			}

			if node.Kind == ast.KindBinaryExpression {
				for _, id := range collectIdentifiersFromBinary(node) {
					if isArrayIndex(id) {
						report(node)
					}
				}
				return
			}

			if node.Kind == ast.KindCallExpression {

				if ast.IsOptionalChain(node) {
					return
				}
				call := node.AsCallExpression()
				ce := ast.SkipParentheses(call.Expression)

				if ce != nil && ce.Kind == ast.KindPropertyAccessExpression {

					if ast.IsOptionalChain(ce) {
						return
					}
					pa := ce.AsPropertyAccessExpression()
					prop := pa.Name()
					if isArrayIndex(pa.Expression) && prop != nil && prop.Kind == ast.KindIdentifier && prop.AsIdentifier().Text == "toString" {
						report(node)
						return
					}
				}

				if ce != nil && ce.Kind == ast.KindIdentifier && ce.AsIdentifier().Text == "String" {
					if call.Arguments != nil && len(call.Arguments.Nodes) > 0 {
						firstArg := call.Arguments.Nodes[0]
						if isArrayIndex(firstArg) {

							report(ast.SkipParentheses(firstArg))
						}
					}
				}
			}
		}

		keyMatchesUpstreamIdentifier := func(keyName *ast.Node) bool {
			if keyName == nil {
				return false
			}

			if keyName.Kind == ast.KindComputedPropertyName {
				inner := keyName.AsComputedPropertyName().Expression
				return inner != nil && inner.Kind == ast.KindIdentifier && inner.AsIdentifier().Text == "key"
			}
			return keyName.Kind == ast.KindIdentifier && keyName.AsIdentifier().Text == "key"
		}

		checkObjectKeyProp := func(props *ast.Node) {
			props = ast.SkipParentheses(props)
			if props == nil || props.Kind != ast.KindObjectLiteralExpression {
				return
			}
			obj := props.AsObjectLiteralExpression()
			if obj.Properties == nil {
				return
			}
			for _, prop := range obj.Properties.Nodes {
				switch prop.Kind {
				case ast.KindPropertyAssignment:
					pa := prop.AsPropertyAssignment()
					if !keyMatchesUpstreamIdentifier(pa.Name()) {
						continue
					}
					if pa.Initializer != nil {
						checkPropValue(pa.Initializer)
					}
				case ast.KindShorthandPropertyAssignment:

					spa := prop.AsShorthandPropertyAssignment()
					nameNode := spa.Name()
					if !keyMatchesUpstreamIdentifier(nameNode) {
						continue
					}
					checkPropValue(nameNode)
				}
			}
		}

		isCreateCloneElement := func(callee *ast.Node) bool {
			if callee == nil {
				return false
			}
			callee = ast.SkipParentheses(callee)
			if callee == nil {
				return false
			}

			if callee.Kind == ast.KindPropertyAccessExpression {
				pa := callee.AsPropertyAccessExpression()
				name := pa.Name()
				if name == nil || name.Kind != ast.KindIdentifier {
					return false
				}
				txt := name.AsIdentifier().Text
				if txt != "createElement" && txt != "cloneElement" {
					return false
				}
				obj := ast.SkipParentheses(pa.Expression)
				return obj != nil && obj.Kind == ast.KindIdentifier && obj.AsIdentifier().Text == pragma
			}

			if callee.Kind == ast.KindIdentifier {
				return isImportSpecifierFromReact(ctx, callee)
			}

			return false
		}

		return rule.RuleListeners{
			ast.KindCallExpression: func(node *ast.Node) {
				call := node.AsCallExpression()

				if isCreateCloneElement(call.Expression) && call.Arguments != nil && len(call.Arguments.Nodes) > 1 {
					if len(indexParamNames) == 0 {
						return
					}
					checkObjectKeyProp(call.Arguments.Nodes[1])
					return
				}

				if name := getMapIndexParamName(call); name != "" {
					indexParamNames = append(indexParamNames, name)
				}
			},
			rule.ListenerOnExit(ast.KindCallExpression): func(node *ast.Node) {
				if name := getMapIndexParamName(node.AsCallExpression()); name != "" && len(indexParamNames) > 0 {
					indexParamNames = indexParamNames[:len(indexParamNames)-1]
				}
			},
			ast.KindJsxAttribute: func(node *ast.Node) {
				attr := node.AsJsxAttribute()
				name := attr.Name()
				if name == nil || name.Kind != ast.KindIdentifier || name.AsIdentifier().Text != "key" {
					return
				}
				if len(indexParamNames) == 0 {
					return
				}
				init := attr.Initializer
				if init == nil || init.Kind != ast.KindJsxExpression {
					return
				}
				expr := init.AsJsxExpression().Expression
				if expr == nil {
					return
				}
				checkPropValue(expr)
			},
		}
	},
}

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