@@ -2230,6 +2230,142 @@ function createFormMethodRequireFix(
22302230 } ;
22312231}
22322232
2233+ /**
2234+ * Create auto-fix action for empty-tag-not-self-closed rule
2235+ *
2236+ * This fixes void HTML elements by converting them to proper self-closing format.
2237+ * Void elements should be self-closing and never have closing tags.
2238+ * Only applies to void HTML elements (br, img, hr, input, etc.).
2239+ *
2240+ * Example transformations:
2241+ * - <br> → <br/>
2242+ * - <img src="test.jpg"> → <img src="test.jpg"/>
2243+ * - <hr> → <hr/>
2244+ */
2245+ function createEmptyTagNotSelfClosedFix (
2246+ document : TextDocument ,
2247+ diagnostic : Diagnostic ,
2248+ ) : CodeAction | null {
2249+ trace (
2250+ `[DEBUG] createEmptyTagNotSelfClosedFix called with diagnostic: ${ JSON . stringify ( diagnostic ) } ` ,
2251+ ) ;
2252+
2253+ if (
2254+ ! diagnostic . data ||
2255+ diagnostic . data . ruleId !== "empty-tag-not-self-closed"
2256+ ) {
2257+ trace (
2258+ `[DEBUG] createEmptyTagNotSelfClosedFix: Invalid diagnostic data or ruleId: ${ JSON . stringify ( diagnostic . data ) } ` ,
2259+ ) ;
2260+ return null ;
2261+ }
2262+
2263+ const text = document . getText ( ) ;
2264+ const diagnosticOffset = document . offsetAt ( diagnostic . range . start ) ;
2265+
2266+ // List of void elements that should be self-closing
2267+ const voidElements = [
2268+ "area" ,
2269+ "base" ,
2270+ "br" ,
2271+ "col" ,
2272+ "embed" ,
2273+ "hr" ,
2274+ "img" ,
2275+ "input" ,
2276+ "link" ,
2277+ "meta" ,
2278+ "source" ,
2279+ "track" ,
2280+ "wbr" ,
2281+ ] ;
2282+
2283+ // Find the tag boundaries around the diagnostic position
2284+ const tagBoundaries = findTagBoundaries ( text , diagnosticOffset ) ;
2285+ if ( ! tagBoundaries ) {
2286+ trace (
2287+ `[DEBUG] createEmptyTagNotSelfClosedFix: Could not find tag boundaries` ,
2288+ ) ;
2289+ return null ;
2290+ }
2291+
2292+ const { tagStart, tagEnd } = tagBoundaries ;
2293+
2294+ // Extract the content around the diagnostic position to find void elements
2295+ const searchStart = Math . max ( 0 , tagStart - 50 ) ;
2296+ const searchEnd = Math . min ( text . length , tagEnd + 50 ) ;
2297+ const searchText = text . substring ( searchStart , searchEnd ) ;
2298+
2299+ // Pattern to match void elements that are not self-closing: <tagname> or <tagname attributes>
2300+ const voidElementPattern = / < ( \w + ) ( \s [ ^ > ] * ?) ? \s * > / gi;
2301+ let match ;
2302+
2303+ while ( ( match = voidElementPattern . exec ( searchText ) ) !== null ) {
2304+ const matchStart = searchStart + match . index ;
2305+ const matchEnd = matchStart + match [ 0 ] . length ;
2306+ const tagName = match [ 1 ] . toLowerCase ( ) ;
2307+ const attributes = match [ 2 ] || "" ;
2308+ const fullMatch = match [ 0 ] ;
2309+
2310+ // Skip if already self-closing
2311+ if ( fullMatch . endsWith ( "/>" ) ) {
2312+ continue ;
2313+ }
2314+
2315+ // Check if this match contains our diagnostic position
2316+ if ( matchStart <= diagnosticOffset && diagnosticOffset <= matchEnd ) {
2317+ // Verify this is a void element
2318+ if ( ! voidElements . includes ( tagName ) ) {
2319+ trace (
2320+ `[DEBUG] createEmptyTagNotSelfClosedFix: ${ tagName } is not a void element` ,
2321+ ) ;
2322+ return null ;
2323+ }
2324+
2325+ trace (
2326+ `[DEBUG] createEmptyTagNotSelfClosedFix: Found non-self-closing ${ tagName } tag at position ${ matchStart } -${ matchEnd } ` ,
2327+ ) ;
2328+
2329+ // Create the self-closing replacement
2330+ const selfClosingTag = attributes . trim ( )
2331+ ? `<${ tagName } ${ attributes } />`
2332+ : `<${ tagName } />` ;
2333+
2334+ const edit : TextEdit = {
2335+ range : {
2336+ start : document . positionAt ( matchStart ) ,
2337+ end : document . positionAt ( matchEnd ) ,
2338+ } ,
2339+ newText : selfClosingTag ,
2340+ } ;
2341+
2342+ trace (
2343+ `[DEBUG] createEmptyTagNotSelfClosedFix: Will replace "${ fullMatch } " with "${ selfClosingTag } "` ,
2344+ ) ;
2345+
2346+ const action = CodeAction . create (
2347+ `Convert ${ tagName } tag to self-closing` ,
2348+ {
2349+ changes : {
2350+ [ document . uri ] : [ edit ] ,
2351+ } ,
2352+ } ,
2353+ CodeActionKind . QuickFix ,
2354+ ) ;
2355+
2356+ action . diagnostics = [ diagnostic ] ;
2357+ action . isPreferred = true ;
2358+
2359+ return action ;
2360+ }
2361+ }
2362+
2363+ trace (
2364+ `[DEBUG] createEmptyTagNotSelfClosedFix: No matching empty tag pair found` ,
2365+ ) ;
2366+ return null ;
2367+ }
2368+
22332369/**
22342370 * Create auto-fix actions for supported rules
22352371 */
@@ -2336,6 +2472,10 @@ async function createAutoFixes(
23362472 trace ( `[DEBUG] Calling createFormMethodRequireFix` ) ;
23372473 fix = createFormMethodRequireFix ( document , diagnostic ) ;
23382474 break ;
2475+ case "empty-tag-not-self-closed" :
2476+ trace ( `[DEBUG] Calling createEmptyTagNotSelfClosedFix` ) ;
2477+ fix = createEmptyTagNotSelfClosedFix ( document , diagnostic ) ;
2478+ break ;
23392479 default :
23402480 trace ( `[DEBUG] No autofix function found for rule: ${ ruleId } ` ) ;
23412481 break ;
0 commit comments