66'use strict' ;
77
88const repeat = require ( 'string.prototype.repeat' ) ;
9+ const has = require ( 'hasown' ) ;
910
1011const astUtil = require ( '../util/ast' ) ;
1112const docsUrl = require ( '../util/docsUrl' ) ;
13+ const getSourceCode = require ( '../util/eslint' ) . getSourceCode ;
1214const report = require ( '../util/report' ) ;
1315
1416// ------------------------------------------------------------------------------
@@ -18,6 +20,14 @@ const report = require('../util/report');
1820const messages = {
1921 onOwnLine : 'Closing tag of a multiline JSX expression must be on its own line.' ,
2022 matchIndent : 'Expected closing tag to match indentation of opening.' ,
23+ alignWithOpening : 'Expected closing tag to be aligned with the line containing the opening tag' ,
24+ } ;
25+
26+ const defaultOption = 'tag-aligned' ;
27+
28+ const optionMessageMap = {
29+ 'tag-aligned' : 'matchIndent' ,
30+ 'line-aligned' : 'alignWithOpening' ,
2131} ;
2232
2333/** @type {import('eslint').Rule.RuleModule } */
@@ -31,31 +41,87 @@ module.exports = {
3141 } ,
3242 fixable : 'whitespace' ,
3343 messages,
44+ schema : [ {
45+ anyOf : [
46+ {
47+ enum : [ 'tag-aligned' , 'line-aligned' ] ,
48+ } ,
49+ {
50+ type : 'object' ,
51+ properties : {
52+ location : {
53+ enum : [ 'tag-aligned' , 'line-aligned' ] ,
54+ } ,
55+ } ,
56+ additionalProperties : false ,
57+ } ,
58+ ] ,
59+ } ] ,
3460 } ,
3561
3662 create ( context ) {
63+ const config = context . options [ 0 ] ;
64+ let option = defaultOption ;
65+
66+ if ( typeof config === 'string' ) {
67+ option = config ;
68+ } else if ( typeof config === 'object' ) {
69+ if ( has ( config , 'location' ) ) {
70+ option = config . location ;
71+ }
72+ }
73+
74+ function getIndentation ( openingStartOfLine , opening ) {
75+ if ( option === 'line-aligned' ) return openingStartOfLine . column ;
76+ if ( option === 'tag-aligned' ) return opening . loc . start . column ;
77+ }
78+
3779 function handleClosingElement ( node ) {
3880 if ( ! node . parent ) {
3981 return ;
4082 }
83+ const sourceCode = getSourceCode ( context ) ;
4184
4285 const opening = node . parent . openingElement || node . parent . openingFragment ;
86+ const openingLoc = sourceCode . getFirstToken ( opening ) . loc . start ;
87+ const openingLine = sourceCode . lines [ openingLoc . line - 1 ] ;
88+
89+ const openingStartOfLine = {
90+ column : / ^ \s * / . exec ( openingLine ) [ 0 ] . length ,
91+ line : openingLoc . line ,
92+ } ;
93+
4394 if ( opening . loc . start . line === node . loc . start . line ) {
4495 return ;
4596 }
4697
47- if ( opening . loc . start . column === node . loc . start . column ) {
98+ if (
99+ opening . loc . start . column === node . loc . start . column
100+ && option === 'tag-aligned'
101+ ) {
102+ return ;
103+ }
104+
105+ if (
106+ openingStartOfLine . column === node . loc . start . column
107+ && option === 'line-aligned'
108+ ) {
48109 return ;
49110 }
50111
51112 const messageId = astUtil . isNodeFirstInLine ( context , node )
52- ? 'matchIndent'
113+ ? optionMessageMap [ option ]
53114 : 'onOwnLine' ;
115+
54116 report ( context , messages [ messageId ] , messageId , {
55117 node,
56118 loc : node . loc ,
57119 fix ( fixer ) {
58- const indent = repeat ( ' ' , opening . loc . start . column ) ;
120+ const indent = repeat (
121+ ' ' ,
122+ getIndentation ( openingStartOfLine , opening )
123+ ) ;
124+
59125 if ( astUtil . isNodeFirstInLine ( context , node ) ) {
60126 return fixer . replaceTextRange (
61127 [ node . range [ 0 ] - node . loc . start . column , node . range [ 0 ] ] ,
0 commit comments