jsx_indent

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

Documentation

Index

Constants

This section is empty.

Variables

View Source
var JsxIndentRule = rule.Rule{
	Name: "react/jsx-indent",
	Run: func(ctx rule.RuleContext, options any) rule.RuleListeners {
		indentType, indentSize, indentChar := parseIndentOption(options)
		checkAttributes, indentLogicalExpressions := parseSecondOption(options)

		text := ctx.SourceFile.Text()
		lineMap := ctx.SourceFile.ECMALineMap()

		lineOfPos := func(pos int) int {
			return scanner.ComputeLineOfPosition(lineMap, pos)
		}

		reportLiteral := func(node *ast.Node, needed, gotten int, fixed string) {
			rawRange := core.NewTextRange(node.Pos(), node.End())
			ctx.ReportRangeWithFixes(rawRange, reactutil.WrongIndentMessage(needed, gotten, indentType), rule.RuleFix{
				Text:  fixed,
				Range: rawRange,
			})
		}

		reportReturn := func(node *ast.Node, needed, gotten int) {
			indent := strings.Repeat(string(indentChar), needed)
			trimmed := utils.TrimNodeTextRange(ctx.SourceFile, node)
			start := trimmed.Pos()
			end := trimmed.End()
			raw := text[start:end]
			msg := reactutil.WrongIndentMessage(needed, gotten, indentType)
			if !strings.Contains(raw, "\n") {
				ctx.ReportNode(node, msg)
				return
			}
			lastNL := strings.LastIndex(raw, "\n")
			lastLine := raw[lastNL:]
			fixedLast := replaceFirstLeadingIndent(lastLine, indent)
			ctx.ReportNodeWithFixes(node, msg, rule.RuleFix{
				Text:  fixedLast,
				Range: core.NewTextRange(start+lastNL, end),
			})
		}

		reportAttribute := func(anchorPos int, needed, gotten int, fixRange core.TextRange, fixText string) {
			anchorRange := core.NewTextRange(anchorPos, anchorPos+1)
			ctx.ReportRangeWithFixes(anchorRange, reactutil.WrongIndentMessage(needed, gotten, indentType), rule.RuleFix{
				Text:  fixText,
				Range: fixRange,
			})
		}

		jsxOperandPosition := func(node *ast.Node) *ast.Node {
			switch node.Kind {
			case ast.KindJsxOpeningElement, ast.KindJsxOpeningFragment:
				return node.Parent
			default:
				return node
			}
		}

		containerAfterWrappers := func(operand *ast.Node) (cur *ast.Node, parent *ast.Node) {
			cur = operand
			parent = cur.Parent
			for parent != nil {
				switch parent.Kind {
				case ast.KindParenthesizedExpression,
					ast.KindAsExpression,
					ast.KindSatisfiesExpression,
					ast.KindNonNullExpression,
					ast.KindTypeAssertionExpression:
					cur = parent
					parent = cur.Parent
					continue
				}
				break
			}
			return cur, parent
		}

		isRightInLogicalExp := func(node *ast.Node) bool {
			if indentLogicalExpressions {
				return false
			}
			operand := jsxOperandPosition(node)
			if operand == nil {
				return false
			}
			cur, parent := containerAfterWrappers(operand)
			if parent == nil || parent.Kind != ast.KindBinaryExpression {
				return false
			}
			be := parent.AsBinaryExpression()
			op := be.OperatorToken.Kind
			if op != ast.KindAmpersandAmpersandToken &&
				op != ast.KindBarBarToken &&
				op != ast.KindQuestionQuestionToken {
				return false
			}
			return be.Right == cur
		}

		isAlternateInConditionalExp := func(node *ast.Node) bool {
			operand := jsxOperandPosition(node)
			if operand == nil {
				return false
			}
			cur, parent := containerAfterWrappers(operand)
			if parent == nil || parent.Kind != ast.KindConditionalExpression {
				return false
			}
			ce := parent.AsConditionalExpression()
			if ce.WhenFalse != cur {
				return false
			}
			trimmed := utils.TrimNodeTextRange(ctx.SourceFile, node)
			i := trimmed.Pos() - 1
			for i >= 0 {
				c := text[i]
				if c == ' ' || c == '\t' || c == '\r' || c == '\n' {
					i--
					continue
				}
				return c != '('
			}
			return true
		}

		checkNodesIndent := func(node *ast.Node, indent int) {
			nodeIndent := reactutil.NodeStartIndent(ctx.SourceFile, node, indentChar)
			isCorrectRightInLogicalExp := isRightInLogicalExp(node) && (nodeIndent-indent) == indentSize
			isCorrectAlternateInCondExp := isAlternateInConditionalExp(node) && (nodeIndent-indent) == 0
			if nodeIndent != indent &&
				reactutil.IsNodeFirstInLine(ctx.SourceFile, node) &&
				!isCorrectRightInLogicalExp &&
				!isCorrectAlternateInCondExp {
				reactutil.ReportIndentReplaceLeading(ctx, node, indent, nodeIndent, indentChar, indentType)
			}
		}

		checkLiteralNodeIndent := func(node *ast.Node, expected int) {
			rawStart := node.Pos()
			rawEnd := node.End()
			value := text[rawStart:rawEnd]
			indents := scanLiteralIndents(value, indentChar)
			if len(indents) == 0 {
				return
			}
			allMatch := true
			for _, ind := range indents {
				if ind != expected {
					allMatch = false
					break
				}
			}
			if allMatch {
				return
			}
			indentStr := strings.Repeat(string(indentChar), expected)
			fixedText := replaceLeadingIndentInText(value, indentStr)
			for _, actualIndent := range indents {
				reportLiteral(node, expected, actualIndent, fixedText)
			}
		}

		commaContainer := func(node *ast.Node) *ast.Node {
			operand := jsxOperandPosition(node)
			if operand == nil {
				return nil
			}
			_, parent := containerAfterWrappers(operand)
			if parent == nil {
				return nil
			}
			switch parent.Kind {
			case ast.KindArrayLiteralExpression,
				ast.KindCallExpression,
				ast.KindNewExpression,
				ast.KindObjectLiteralExpression:
				return parent
			}
			return nil
		}

		colonAnchor := func(node *ast.Node) *ast.Node {
			operand := jsxOperandPosition(node)
			if operand == nil {
				return nil
			}
			cur := operand
			parent := cur.Parent
			for parent != nil {
				if parent.Kind == ast.KindConditionalExpression {
					ce := parent.AsConditionalExpression()
					if ce.WhenFalse == cur {
						return reactutil.SkipExpressionWrappers(ce.WhenTrue)
					}
					return nil
				}
				switch parent.Kind {
				case ast.KindParenthesizedExpression,
					ast.KindAsExpression,
					ast.KindSatisfiesExpression,
					ast.KindNonNullExpression,
					ast.KindTypeAssertionExpression:
					cur = parent
					parent = cur.Parent
					continue
				}
				return nil
			}
			return nil
		}

		jsxParentOpening := func(node *ast.Node) *ast.Node {
			operand := jsxOperandPosition(node)
			if operand == nil {
				return nil
			}
			parent := operand.Parent
			if parent == nil {
				return nil
			}
			switch parent.Kind {
			case ast.KindJsxElement:
				return parent.AsJsxElement().OpeningElement
			case ast.KindJsxFragment:
				return parent.AsJsxFragment().OpeningFragment
			}
			return nil
		}

		previousAnchorIndent := func(node *ast.Node) (int, bool, bool) {
			trimmed := utils.TrimNodeTextRange(ctx.SourceFile, node)
			startPos := trimmed.Pos()

			if opening := jsxParentOpening(node); opening != nil {
				openingTrimmed := utils.TrimNodeTextRange(ctx.SourceFile, opening)
				sameLine := lineOfPos(openingTrimmed.Pos()) == lineOfPos(startPos)
				return reactutil.IndentLeading(text, lineMap, openingTrimmed.Pos(), indentChar), sameLine, true
			}

			i := startPos - 1
			for i >= 0 && (text[i] == ' ' || text[i] == '\t' || text[i] == '\r' || text[i] == '\n') {
				i--
			}
			if i < 0 {
				return 0, false, false
			}
			if text[i] == ',' {
				container := commaContainer(node)
				if container != nil {
					containerTrimmed := utils.TrimNodeTextRange(ctx.SourceFile, container)
					sameLine := lineOfPos(containerTrimmed.Pos()) == lineOfPos(startPos)
					return reactutil.IndentLeading(text, lineMap, containerTrimmed.Pos(), indentChar), sameLine, true
				}
				i--
				for i >= 0 && (text[i] == ' ' || text[i] == '\t' || text[i] == '\r' || text[i] == '\n') {
					i--
				}
				if i < 0 {
					return 0, false, true
				}
				return reactutil.IndentLeading(text, lineMap, i, indentChar), false, true
			}
			if text[i] == ':' {
				anchor := colonAnchor(node)
				if anchor != nil {
					anchorTrimmed := utils.TrimNodeTextRange(ctx.SourceFile, anchor)
					sameLine := lineOfPos(anchorTrimmed.Pos()) == lineOfPos(startPos)
					return reactutil.IndentLeading(text, lineMap, anchorTrimmed.Pos(), indentChar), sameLine, true
				}

			}
			sameLine := lineOfPos(i) == lineOfPos(startPos)
			return reactutil.IndentLeading(text, lineMap, i, indentChar), sameLine, true
		}

		handleOpeningElement := func(node *ast.Node) {
			parentIndent, sameLine, ok := previousAnchorIndent(node)
			if !ok {
				return
			}
			additional := indentSize
			if sameLine ||
				isRightInLogicalExp(node) ||
				isAlternateInConditionalExp(node) {
				additional = 0
			}
			checkNodesIndent(node, parentIndent+additional)
		}

		handleClosingElement := func(node *ast.Node) {
			parent := node.Parent
			if parent == nil {
				return
			}
			var openingNode *ast.Node
			switch parent.Kind {
			case ast.KindJsxElement:
				openingNode = parent.AsJsxElement().OpeningElement
			case ast.KindJsxFragment:
				openingNode = parent.AsJsxFragment().OpeningFragment
			default:
				return
			}
			peerIndent := reactutil.NodeStartIndent(ctx.SourceFile, openingNode, indentChar)
			checkNodesIndent(node, peerIndent)
		}

		handleAttribute := func(node *ast.Node) {
			if !checkAttributes {
				return
			}
			attr := node.AsJsxAttribute()
			if attr == nil || attr.Initializer == nil || attr.Initializer.Kind != ast.KindJsxExpression {
				return
			}
			nameNode := attr.Name()
			if nameNode == nil {
				return
			}
			value := attr.Initializer
			je := value.AsJsxExpression()
			if je == nil || je.Expression == nil {
				return
			}

			closeBracePos := value.End() - 1
			i := closeBracePos - 1
			for i >= 0 && (text[i] == ' ' || text[i] == '\t' || text[i] == '\r' || text[i] == '\n') {
				i--
			}
			if i < 0 {
				return
			}
			anchorPos := i
			lineStart := reactutil.IndentLineStart(lineMap, anchorPos)
			actualIndent := 0
			for j := lineStart; j < anchorPos; j++ {
				if text[j] != indentChar {
					break
				}
				actualIndent++
			}

			isFirst := true
			for j := anchorPos - 1; j >= lineStart; j-- {
				c := text[j]
				if c != ' ' && c != '\t' && c != ',' && c != '\r' {
					isFirst = false
					break
				}
			}
			if !isFirst {
				return
			}
			nameLine := lineOfPos(nameNode.Pos())
			anchorLine := lineOfPos(anchorPos)
			expectedIndent := reactutil.NodeStartIndent(ctx.SourceFile, nameNode, indentChar)
			if nameLine == anchorLine {
				expectedIndent = 0
			}
			if actualIndent == expectedIndent {
				return
			}
			indentStr := strings.Repeat(string(indentChar), expectedIndent)
			fixRange := core.NewTextRange(lineStart, anchorPos)
			reportAttribute(anchorPos, expectedIndent, actualIndent, fixRange, indentStr)
		}

		handleJsxExpression := func(node *ast.Node) {
			parent := node.Parent
			if parent == nil {
				return
			}
			parentIndent := reactutil.NodeStartIndent(ctx.SourceFile, parent, indentChar)
			checkNodesIndent(node, parentIndent+indentSize)
		}

		handleJsxText := func(node *ast.Node) {
			parent := node.Parent
			if parent == nil {
				return
			}
			if parent.Kind != ast.KindJsxElement && parent.Kind != ast.KindJsxFragment {
				return
			}
			parentIndent := reactutil.NodeStartIndent(ctx.SourceFile, parent, indentChar)
			checkLiteralNodeIndent(node, parentIndent+indentSize)
		}

		handleReturn := func(node *ast.Node) {
			ret := node.AsReturnStatement()
			if ret == nil || ret.Expression == nil {
				return
			}

			arg := ast.SkipParentheses(ret.Expression)
			if !reactutil.IsJsxLike(arg) {
				return
			}
			fn := node.Parent
			for fn != nil &&
				fn.Kind != ast.KindFunctionDeclaration &&
				fn.Kind != ast.KindFunctionExpression {
				fn = fn.Parent
			}
			if fn == nil {
				return
			}
			openingIndent := reactutil.NodeStartIndent(ctx.SourceFile, node, indentChar)
			closingIndent := reactutil.NodeEndIndent(ctx.SourceFile, node, indentChar)
			if openingIndent != closingIndent {
				reportReturn(node, openingIndent, closingIndent)
			}
		}

		return rule.RuleListeners{
			ast.KindJsxOpeningElement:     handleOpeningElement,
			ast.KindJsxSelfClosingElement: handleOpeningElement,
			ast.KindJsxOpeningFragment:    handleOpeningElement,
			ast.KindJsxClosingElement:     handleClosingElement,
			ast.KindJsxClosingFragment:    handleClosingElement,
			ast.KindJsxAttribute:          handleAttribute,
			ast.KindJsxExpression:         handleJsxExpression,
			ast.KindJsxText:               handleJsxText,
			ast.KindReturnStatement:       handleReturn,
		}
	},
}

JsxIndentRule enforces JSX indentation.

Ported from eslint-plugin-react's `jsx-indent` rule. Each opening / closing tag, JSXExpressionContainer, JSXText and JSX-returning ReturnStatement is checked against a parent-derived "expected indent"; violations carry an autofix that rewrites the leading whitespace of the offending line.

tsgo↔ESTree shape adjustments (vs the upstream JS rule):

  • Self-closing `<Foo />` is `JsxSelfClosingElement` with no wrapping `JsxElement`, so the listener resolves the "operand position" via `jsxOperandPosition(node)` before walking up the tree.
  • Parens / `as` / `satisfies` / `!` are explicit nodes; we use `reactutil.SkipExpressionWrappersUp` to flatten them when matching LogicalExpression / ConditionalExpression ancestors.
  • Token positions for source-level scans (e.g. "is the previous char a colon") are computed from `SourceFile.Text()`; line numbers from `ECMALineMap()` + `scanner.ComputeLineOfPosition`.

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