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.
Click to show internal directories.
Click to hide internal directories.