Skip to content

Commit 30b6335

Browse files
Copilotbrettz9
andcommitted
Add support for preserving list indentation in check-line-alignment (never mode)
Co-authored-by: brettz9 <[email protected]>
1 parent 99c7fbd commit 30b6335

File tree

3 files changed

+190
-2
lines changed

3 files changed

+190
-2
lines changed

src/alignTransform.js

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,27 @@ import {
99
util,
1010
} from 'comment-parser';
1111

12+
/**
13+
* Detects if a line starts with a markdown list marker
14+
* Supports: -, *, numbered lists (1., 2., etc.)
15+
* This explicitly excludes hyphens that are part of JSDoc tag syntax
16+
* @param {string} text - The text to check
17+
* @param {boolean} isFirstLineOfTag - True if this is the first line (tag line)
18+
* @returns {boolean} - True if the text starts with a list marker
19+
*/
20+
const startsWithListMarker = (text, isFirstLineOfTag = false) => {
21+
// On the first line of a tag, the hyphen is typically the JSDoc separator,
22+
// not a list marker
23+
if (isFirstLineOfTag) {
24+
return false;
25+
}
26+
27+
// Match lines that start with optional whitespace, then a list marker
28+
// - or * followed by a space
29+
// or a number followed by . or ) and a space
30+
return /^\s*(?:[\-*]|\d+(?:\.|\)))\s+/v.test(text);
31+
};
32+
1233
/**
1334
* @typedef {{
1435
* hasNoTypes: boolean,
@@ -315,9 +336,45 @@ const alignTransform = ({
315336

316337
// Not align.
317338
if (shouldAlign(tags, index, source)) {
339+
// Save original postDelimiter before alignTokens modifies it
340+
const originalPostDelimiter = tokens.postDelimiter;
341+
// Remove the delimiter space
342+
const originalIndent = tokens.postDelimiter.slice(1);
343+
318344
alignTokens(tokens, typelessInfo);
345+
319346
if (!disableWrapIndent && indentTag) {
320-
tokens.postDelimiter += wrapIndent;
347+
// Preserve extra indentation for list continuation lines
348+
// Check if any previous line (or current) in the current tag has a list marker
349+
let hasListMarker = false;
350+
351+
// Find the start of the current tag
352+
let tagStartIndex = index;
353+
while (tagStartIndex > 0 && source[tagStartIndex].tokens.tag === '') {
354+
tagStartIndex--;
355+
}
356+
357+
// Check all lines from tag start to current line for list markers
358+
for (let idx = tagStartIndex; idx <= index; idx++) {
359+
const isFirstLine = (idx === tagStartIndex);
360+
if (source[idx]?.tokens?.description && startsWithListMarker(source[idx].tokens.description, isFirstLine)) {
361+
hasListMarker = true;
362+
break;
363+
}
364+
}
365+
366+
// If we're in a list context and this line has extra indentation beyond wrapIndent,
367+
// preserve the original indentation
368+
const hasExtraIndent = originalIndent.length > wrapIndent.length &&
369+
originalIndent.startsWith(wrapIndent);
370+
371+
if (hasListMarker && hasExtraIndent) {
372+
// Preserve the original indentation completely
373+
tokens.postDelimiter = originalPostDelimiter;
374+
} else {
375+
// Normal case: add wrapIndent after the aligned delimiter
376+
tokens.postDelimiter += wrapIndent;
377+
}
321378
}
322379
}
323380

src/rules/checkLineAlignment.js

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,54 @@ const {
88
flow: commentFlow,
99
} = transforms;
1010

11+
/**
12+
* Detects if a line starts with a markdown list marker
13+
* Supports: -, *, numbered lists (1., 2., etc.)
14+
* This explicitly excludes hyphens that are part of JSDoc tag syntax
15+
* @param {string} text - The text to check
16+
* @param {boolean} isFirstLineOfTag - True if this is the first line (tag line)
17+
* @returns {boolean} - True if the text starts with a list marker
18+
*/
19+
const startsWithListMarker = (text, isFirstLineOfTag = false) => {
20+
// On the first line of a tag, the hyphen is typically the JSDoc separator,
21+
// not a list marker
22+
if (isFirstLineOfTag) {
23+
return false;
24+
}
25+
26+
// Match lines that start with optional whitespace, then a list marker
27+
// - or * followed by a space
28+
// or a number followed by . or ) and a space
29+
return /^\s*(?:[\-*]|\d+(?:\.|\)))\s+/v.test(text);
30+
};
31+
32+
/**
33+
* Checks if we should allow extra indentation beyond wrapIndent.
34+
* This is true for list continuation lines (lines with more indent than wrapIndent
35+
* that follow a list item).
36+
* @param {import('comment-parser').Spec} tag - The tag being checked
37+
* @param {import('../iterateJsdoc.js').Integer} idx - Current line index (0-based in tag.source.slice(1))
38+
* @returns {boolean} - True if extra indentation should be allowed
39+
*/
40+
const shouldAllowExtraIndent = (tag, idx) => {
41+
// Check if any previous line in this tag had a list marker
42+
// idx is 0-based in the continuation lines (tag.source.slice(1))
43+
// So tag.source[0] is the tag line, tag.source[idx+1] is current line
44+
let hasSeenListMarker = false;
45+
46+
// Check all lines from the tag line onwards
47+
for (let lineIdx = 0; lineIdx <= idx + 1; lineIdx++) {
48+
const line = tag.source[lineIdx];
49+
const isFirstLine = lineIdx === 0;
50+
if (line?.tokens?.description && startsWithListMarker(line.tokens.description, isFirstLine)) {
51+
hasSeenListMarker = true;
52+
break;
53+
}
54+
}
55+
56+
return hasSeenListMarker;
57+
};
58+
1159
/**
1260
* @typedef {{
1361
* postDelimiter: import('../iterateJsdoc.js').Integer,
@@ -298,7 +346,17 @@ export default iterateJsdoc(({
298346
}
299347

300348
// Don't include a single separating space/tab
301-
if (!disableWrapIndent && tokens.postDelimiter.slice(1) !== wrapIndent) {
349+
const actualIndent = tokens.postDelimiter.slice(1);
350+
const hasCorrectWrapIndent = actualIndent === wrapIndent;
351+
352+
// Allow extra indentation if this line or previous lines contain list markers
353+
// This preserves nested list structure
354+
const hasExtraIndent = actualIndent.length > wrapIndent.length &&
355+
actualIndent.startsWith(wrapIndent);
356+
const isInListContext = shouldAllowExtraIndent(tag, idx - 1);
357+
358+
if (!disableWrapIndent && !hasCorrectWrapIndent &&
359+
!(hasExtraIndent && isInListContext)) {
302360
utils.reportJSDoc('Expected wrap indent', {
303361
line: tag.source[0].number + idx,
304362
}, () => {

test/rules/assertions/checkLineAlignment.js

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2167,5 +2167,78 @@ export default /** @type {import('../index.js').TestCases} */ ({
21672167
},
21682168
],
21692169
},
2170+
// List indentation tests - should preserve nested list structure
2171+
{
2172+
code: `
2173+
/**
2174+
* @return {Promise} A promise.
2175+
* - On success, resolves.
2176+
* - On error, rejects with details:
2177+
* - When aborted, textStatus is "abort".
2178+
* - On timeout, textStatus is "timeout".
2179+
*/
2180+
function test() {}
2181+
`,
2182+
options: [
2183+
'never',
2184+
{
2185+
wrapIndent: ' ',
2186+
},
2187+
],
2188+
},
2189+
{
2190+
code: `
2191+
/**
2192+
* @param {string} lorem Description with list:
2193+
* - First item
2194+
* - Second item
2195+
* - Nested item
2196+
* - Another nested item
2197+
*/
2198+
function test() {}
2199+
`,
2200+
options: [
2201+
'never',
2202+
{
2203+
wrapIndent: ' ',
2204+
},
2205+
],
2206+
},
2207+
{
2208+
code: `
2209+
/**
2210+
* @return {Promise} A promise.
2211+
* 1. First step
2212+
* 2. Second step with continuation
2213+
* on another line
2214+
* 3. Third step
2215+
*/
2216+
function test() {}
2217+
`,
2218+
options: [
2219+
'never',
2220+
{
2221+
wrapIndent: ' ',
2222+
},
2223+
],
2224+
},
2225+
{
2226+
code: `
2227+
/**
2228+
* @param {Object} options Configuration options.
2229+
* * First option
2230+
* * Second option with details:
2231+
* * Nested detail
2232+
* * Another detail
2233+
*/
2234+
function test() {}
2235+
`,
2236+
options: [
2237+
'never',
2238+
{
2239+
wrapIndent: ' ',
2240+
},
2241+
],
2242+
},
21702243
],
21712244
});

0 commit comments

Comments
 (0)