@@ -7,7 +7,14 @@ import (
77 "github.com/web-infra-dev/rslint/internal/utils"
88)
99
10- // JsxIndentPropsRule enforces JSX prop indentation.
10+ // JsxIndentPropsRule is the eslint-plugin-react variant of jsx-indent-props.
11+ var JsxIndentPropsRule = BuildRule ("react/jsx-indent-props" )
12+
13+ // BuildRule constructs the jsx-indent-props rule registered under name. Both
14+ // the react (`react/jsx-indent-props`) and @stylistic
15+ // (`@stylistic/jsx-indent-props`) variants are produced from this single
16+ // implementation — the two upstream rules are byte-identical (the @stylistic
17+ // fork carries no behavioral delta), so only the registered Name differs.
1118//
1219// Ported from eslint-plugin-react's `jsx-indent-props` rule. The rule walks
1320// every JsxOpeningElement / JsxSelfClosingElement, computes the expected
@@ -24,146 +31,159 @@ import (
2431// ECMA line map via `scanner.ComputeLineOfPosition`.
2532//
2633// Ternary operator handling: upstream maintains a `line.isUsingOperator`
27- // flag that is set when the line containing the JSX `<` starts with `?` or
28- // `:` after whitespace. When that flag is on (and `'first'` mode is off and
29- // `ignoreTernaryOperator` is off), the FIRST prop that sits first-in-line
30- // receives an extra `indentSize` bump, after which the flag is cleared.
31- // We replicate that behaviour with a per-element `bumpApplied` boolean and
32- // a per-prop bracket reset (mirrors upstream's `useBracket` reset inside
33- // `getNodeIndent` — any prop whose first source line contains `<` cancels
34- // the pending bump, exactly the same way upstream's per-call side effect
35- // would).
36- var JsxIndentPropsRule = rule.Rule {
37- Name : "react/jsx-indent-props" ,
38- Run : func (ctx rule.RuleContext , options any ) rule.RuleListeners {
39- indentType , indentSize , indentChar , indentIsFirst , ignoreTernaryOperator := parseOptions (options )
40-
41- text := ctx .SourceFile .Text ()
42- lineMap := ctx .SourceFile .ECMALineMap ()
43-
44- // firstNonWhitespaceCharOnLine returns the first non space / tab
45- // character on the line containing pos, or 0 if none.
46- firstNonWhitespaceCharOnLine := func (pos int ) byte {
47- start := reactutil .IndentLineStart (lineMap , pos )
48- for i := start ; i < len (text ); i ++ {
49- c := text [i ]
50- if c == ' ' || c == '\t' {
51- continue
52- }
53- if c == '\n' || c == '\r' {
54- return 0
34+ // flag, updated by `getNodeIndent` from each scanned line, that grants the
35+ // next first-in-line prop an extra `indentSize` bump (when `'first'` mode and
36+ // `ignoreTernaryOperator` are both off). We replicate `getNodeIndent`'s
37+ // per-prop side effect with the exact same precedence: a line starting with
38+ // `?`/`:` after whitespace (useOperator) sets the flag and PRECEDES the
39+ // `<`-contains reset (useBracket). That precedence is load-bearing for the
40+ // shape where the first prop shares the opening tag's `?`/`:` line — the line
41+ // both starts with the operator and contains `<`, and upstream keeps the flag
42+ // set so the bump carries to the following props.
43+ func BuildRule (name string ) rule.Rule {
44+ return rule.Rule {
45+ Name : name ,
46+ Run : func (ctx rule.RuleContext , options any ) rule.RuleListeners {
47+ indentType , indentSize , indentChar , indentIsFirst , ignoreTernaryOperator := parseOptions (options )
48+
49+ text := ctx .SourceFile .Text ()
50+ lineMap := ctx .SourceFile .ECMALineMap ()
51+
52+ // firstNonWhitespaceCharOnLine returns the first non space / tab
53+ // character on the line containing pos, or 0 if none.
54+ firstNonWhitespaceCharOnLine := func (pos int ) byte {
55+ start := reactutil .IndentLineStart (lineMap , pos )
56+ for i := start ; i < len (text ); i ++ {
57+ c := text [i ]
58+ if c == ' ' || c == '\t' {
59+ continue
60+ }
61+ if c == '\n' || c == '\r' {
62+ return 0
63+ }
64+ return c
5565 }
56- return c
66+ return 0
5767 }
58- return 0
59- }
6068
61- // firstLineContainsBracket reports whether the first source line
62- // of node (from line-start of trimmed start to the next newline
63- // or the trimmed end, whichever comes first) contains a `<`.
64- // Mirrors upstream's `useBracket` check inside `getNodeIndent`,
65- // which resets `isUsingOperator` whenever the scanned line
66- // includes a `<` (i.e. the prop is on the same line as the
67- // element's opening `<`, or carries an inline JSX-literal value).
68- firstLineContainsBracket := func (node * ast.Node ) bool {
69- trimmed := utils .TrimNodeTextRange (ctx .SourceFile , node )
70- lineStart := reactutil .IndentLineStart (lineMap , trimmed .Pos ())
71- end := trimmed .End ()
72- for i := lineStart ; i < end && i < len (text ); i ++ {
73- c := text [i ]
74- if c == '\n' {
75- break
76- }
77- if c == '<' {
78- return true
69+ // firstLineContainsBracket reports whether the first source line
70+ // of node (from line-start of trimmed start to the next newline
71+ // or the trimmed end, whichever comes first) contains a `<`.
72+ // Mirrors upstream's `useBracket` check inside `getNodeIndent`,
73+ // which resets `isUsingOperator` whenever the scanned line
74+ // includes a `<` (i.e. the prop is on the same line as the
75+ // element's opening `<`, or carries an inline JSX-literal value).
76+ firstLineContainsBracket := func (node * ast.Node ) bool {
77+ trimmed := utils .TrimNodeTextRange (ctx .SourceFile , node )
78+ lineStart := reactutil .IndentLineStart (lineMap , trimmed .Pos ())
79+ end := trimmed .End ()
80+ for i := lineStart ; i < end && i < len (text ); i ++ {
81+ c := text [i ]
82+ if c == '\n' {
83+ break
84+ }
85+ if c == '<' {
86+ return true
87+ }
7988 }
89+ return false
8090 }
81- return false
82- }
8391
84- check := func (node * ast.Node ) {
85- var props []* ast.Node
86- switch node .Kind {
87- case ast .KindJsxOpeningElement :
88- opening := node .AsJsxOpeningElement ()
89- if opening .Attributes != nil {
90- attrs := opening .Attributes .AsJsxAttributes ()
91- if attrs != nil && attrs .Properties != nil {
92- props = attrs .Properties .Nodes
92+ check := func (node * ast.Node ) {
93+ var props []* ast.Node
94+ switch node .Kind {
95+ case ast .KindJsxOpeningElement :
96+ opening := node .AsJsxOpeningElement ()
97+ if opening .Attributes != nil {
98+ attrs := opening .Attributes .AsJsxAttributes ()
99+ if attrs != nil && attrs .Properties != nil {
100+ props = attrs .Properties .Nodes
101+ }
93102 }
94- }
95- case ast . KindJsxSelfClosingElement :
96- self := node . AsJsxSelfClosingElement ()
97- if self .Attributes != nil {
98- attrs := self . Attributes . AsJsxAttributes ()
99- if attrs != nil && attrs .Properties != nil {
100- props = attrs . Properties . Nodes
103+ case ast . KindJsxSelfClosingElement :
104+ self := node . AsJsxSelfClosingElement ()
105+ if self . Attributes != nil {
106+ attrs := self .Attributes . AsJsxAttributes ()
107+ if attrs != nil && attrs . Properties != nil {
108+ props = attrs .Properties . Nodes
109+ }
101110 }
102111 }
103- }
104- if len (props ) == 0 {
105- return
106- }
107-
108- openingTrimmed := utils .TrimNodeTextRange (ctx .SourceFile , node )
109- openingStart := openingTrimmed .Pos ()
110-
111- // isUsingOperator: line containing the opening `<` starts
112- // with `?` or `:` after whitespace.
113- leadChar := firstNonWhitespaceCharOnLine (openingStart )
114- isUsingOperator := leadChar == '?' || leadChar == ':'
115-
116- elementIndent := reactutil .IndentLeading (text , lineMap , openingStart , indentChar )
117-
118- var propIndent int
119- if indentIsFirst {
120- // 'first' mode aligns to the visual column of the
121- // first prop. Use UTF-16 character column (matches
122- // ESLint's `loc.start.column`) instead of byte
123- // offset so multi-byte characters preceding the
124- // first prop don't inflate the expected indent.
125- propIndent = reactutil .NodeStartUTF16Column (ctx .SourceFile , props [0 ])
126- } else {
127- propIndent = elementIndent + indentSize
128- }
129-
130- nestedIndent := propIndent
131- bumpApplied := false
132-
133- for _ , prop := range props {
134- // Mirror upstream: `getNodeIndent(node)` is called per
135- // prop regardless of first-in-line status, and resets
136- // `isUsingOperator` when the scanned line contains a
137- // `<`. Apply the same reset here so a prop whose first
138- // source line includes `<` (e.g. on the element's own
139- // `<` line, or carrying an inline JSX-literal value)
140- // cancels the bump.
141- if firstLineContainsBracket (prop ) {
142- isUsingOperator = false
112+ if len (props ) == 0 {
113+ return
143114 }
144115
145- if ! reactutil .IsNodeFirstInLine (ctx .SourceFile , prop ) {
146- continue
116+ openingTrimmed := utils .TrimNodeTextRange (ctx .SourceFile , node )
117+ openingStart := openingTrimmed .Pos ()
118+
119+ // isUsingOperator: line containing the opening `<` starts
120+ // with `?` or `:` after whitespace.
121+ leadChar := firstNonWhitespaceCharOnLine (openingStart )
122+ isUsingOperator := leadChar == '?' || leadChar == ':'
123+
124+ elementIndent := reactutil .IndentLeading (text , lineMap , openingStart , indentChar )
125+
126+ var propIndent int
127+ if indentIsFirst {
128+ // 'first' mode aligns to the visual column of the
129+ // first prop. Use UTF-16 character column (matches
130+ // ESLint's `loc.start.column`) instead of byte
131+ // offset so multi-byte characters preceding the
132+ // first prop don't inflate the expected indent.
133+ propIndent = reactutil .NodeStartUTF16Column (ctx .SourceFile , props [0 ])
134+ } else {
135+ propIndent = elementIndent + indentSize
147136 }
148137
149- if ! bumpApplied && isUsingOperator && ! indentIsFirst && ! ignoreTernaryOperator {
150- nestedIndent += indentSize
151- bumpApplied = true
152- isUsingOperator = false
153- }
138+ nestedIndent := propIndent
139+
140+ for _ , prop := range props {
141+ // Mirror upstream's getNodeIndent side effect, which runs
142+ // for EVERY prop (before the first-in-line report gate). It
143+ // updates the ternary-operator state from the prop's first
144+ // source line, with useOperator (line starts with `?`/`:`
145+ // after whitespace) taking PRIORITY over useBracket (line
146+ // contains `<`) — exactly upstream's if/else ordering. The
147+ // priority matters when the first prop shares the opening
148+ // tag's `?`/`:` line: that line both starts with the
149+ // operator and contains `<`, and upstream keeps
150+ // isUsingOperator set so the bump carries to the next prop.
151+ propLead := firstNonWhitespaceCharOnLine (utils .TrimNodeTextRange (ctx .SourceFile , prop ).Pos ())
152+ currentOperator := false
153+ if propLead == '?' || propLead == ':' {
154+ isUsingOperator = true
155+ currentOperator = true
156+ } else if firstLineContainsBracket (prop ) {
157+ isUsingOperator = false
158+ }
159+
160+ // Bump decision, also run for every prop. A prop whose own
161+ // line starts with the operator (currentOperator) does not
162+ // consume the bump; the first following prop that doesn't
163+ // re-arm the operator does. Applying it clears
164+ // isUsingOperator so the bump fires at most once.
165+ if isUsingOperator && ! currentOperator && ! indentIsFirst && ! ignoreTernaryOperator {
166+ nestedIndent += indentSize
167+ isUsingOperator = false
168+ }
169+
170+ if ! reactutil .IsNodeFirstInLine (ctx .SourceFile , prop ) {
171+ continue
172+ }
154173
155- actualIndent := reactutil .NodeStartIndent (ctx .SourceFile , prop , indentChar )
156- if actualIndent != nestedIndent {
157- reactutil .ReportIndentReplaceLeading (ctx , prop , nestedIndent , actualIndent , indentChar , indentType )
174+ actualIndent := reactutil .NodeStartIndent (ctx .SourceFile , prop , indentChar )
175+ if actualIndent != nestedIndent {
176+ reactutil .ReportIndentReplaceLeading (ctx , prop , nestedIndent , actualIndent , indentChar , indentType )
177+ }
158178 }
159179 }
160- }
161180
162- return rule.RuleListeners {
163- ast .KindJsxOpeningElement : check ,
164- ast .KindJsxSelfClosingElement : check ,
165- }
166- },
181+ return rule.RuleListeners {
182+ ast .KindJsxOpeningElement : check ,
183+ ast .KindJsxSelfClosingElement : check ,
184+ }
185+ },
186+ }
167187}
168188
169189// parseOptions parses the rule options. The first positional option may be:
0 commit comments