Skip to content

Commit af2d940

Browse files
Copilotbrettz9
andcommitted
Implement "always" mode support for list indentation preservation
Co-authored-by: brettz9 <[email protected]>
1 parent da2c872 commit af2d940

File tree

3 files changed

+172
-1
lines changed

3 files changed

+172
-1
lines changed

docs/rules/check-line-alignment.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1093,5 +1093,35 @@ function test() {}
10931093
*/
10941094
function test() {}
10951095
// "jsdoc/check-line-alignment": ["error"|"warn", "never",{"wrapIndent":" "}]
1096+
1097+
/**
1098+
* @param {string} param Description with list:
1099+
* - Item 1
1100+
* - Nested item
1101+
*/
1102+
function test(param) {}
1103+
// "jsdoc/check-line-alignment": ["error"|"warn", "always",{"wrapIndent":" "}]
1104+
1105+
/**
1106+
* Function description.
1107+
*
1108+
* @param {string} lorem Description.
1109+
* @param {int} sit Description with list:
1110+
* - First item
1111+
* - Second item
1112+
* - Nested item
1113+
*/
1114+
const fn = ( lorem, sit ) => {}
1115+
// "jsdoc/check-line-alignment": ["error"|"warn", "always",{"wrapIndent":" "}]
1116+
1117+
/**
1118+
* @return {Promise} A promise.
1119+
* - On success, resolves.
1120+
* - On error, rejects with details:
1121+
* - When aborted, status is "abort".
1122+
* - On timeout, status is "timeout".
1123+
*/
1124+
function test() {}
1125+
// "jsdoc/check-line-alignment": ["error"|"warn", "always",{"wrapIndent":" "}]
10961126
````
10971127

src/alignTransform.js

Lines changed: 87 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,
@@ -144,6 +165,59 @@ const space = (len) => {
144165
return ''.padStart(len, ' ');
145166
};
146167

168+
/**
169+
* Check if a tag or any of its lines contain list markers
170+
* @param {import('./iterateJsdoc.js').Integer} index - Current line index
171+
* @param {import('comment-parser').Line[]} source - All source lines
172+
* @returns {{hasListMarker: boolean, tagStartIndex: import('./iterateJsdoc.js').Integer}}
173+
*/
174+
const checkForListMarkers = (index, source) => {
175+
let hasListMarker = false;
176+
let tagStartIndex = index;
177+
while (tagStartIndex > 0 && source[tagStartIndex].tokens.tag === '') {
178+
tagStartIndex--;
179+
}
180+
181+
for (let idx = tagStartIndex; idx <= index; idx++) {
182+
const isFirstLine = (idx === tagStartIndex);
183+
if (source[idx]?.tokens?.description && startsWithListMarker(source[idx].tokens.description, isFirstLine)) {
184+
hasListMarker = true;
185+
break;
186+
}
187+
}
188+
189+
return {
190+
hasListMarker,
191+
tagStartIndex,
192+
};
193+
};
194+
195+
/**
196+
* Calculate extra indentation for list items relative to the first continuation line
197+
* @param {import('./iterateJsdoc.js').Integer} index - Current line index
198+
* @param {import('./iterateJsdoc.js').Integer} tagStartIndex - Index of the tag line
199+
* @param {import('comment-parser').Line[]} source - All source lines
200+
* @returns {string} - Extra indentation spaces
201+
*/
202+
const calculateListExtraIndent = (index, tagStartIndex, source) => {
203+
// Find the first continuation line to use as baseline
204+
let firstContinuationIndent = null;
205+
for (let idx = tagStartIndex + 1; idx < source.length; idx++) {
206+
if (source[idx].tokens.description && !source[idx].tokens.tag) {
207+
firstContinuationIndent = source[idx].tokens.postDelimiter.length;
208+
break;
209+
}
210+
}
211+
212+
// Calculate the extra indentation of current line relative to the first continuation line
213+
const currentOriginalIndent = source[index].tokens.postDelimiter.length;
214+
const extraIndent = firstContinuationIndent !== null && currentOriginalIndent > firstContinuationIndent ?
215+
' '.repeat(currentOriginalIndent - firstContinuationIndent) :
216+
'';
217+
218+
return extraIndent;
219+
};
220+
147221
/**
148222
* @param {{
149223
* customSpacings: import('../src/rules/checkLineAlignment.js').CustomSpacings,
@@ -316,8 +390,20 @@ const alignTransform = ({
316390
// Not align.
317391
if (shouldAlign(tags, index, source)) {
318392
alignTokens(tokens, typelessInfo);
393+
319394
if (!disableWrapIndent && indentTag) {
320-
tokens.postDelimiter += wrapIndent;
395+
const {
396+
hasListMarker,
397+
tagStartIndex,
398+
} = checkForListMarkers(index, source);
399+
400+
if (hasListMarker && index > tagStartIndex) {
401+
const extraIndent = calculateListExtraIndent(index, tagStartIndex, source);
402+
tokens.postDelimiter += wrapIndent + extraIndent;
403+
} else {
404+
// Normal case: add wrapIndent after the aligned delimiter
405+
tokens.postDelimiter += wrapIndent;
406+
}
321407
}
322408
}
323409

test/rules/assertions/checkLineAlignment.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2240,5 +2240,60 @@ export default /** @type {import('../index.js').TestCases} */ ({
22402240
},
22412241
],
22422242
},
2243+
// Test cases for "always" mode with list indentation
2244+
{
2245+
code: `
2246+
/**
2247+
* @param {string} param Description with list:
2248+
* - Item 1
2249+
* - Nested item
2250+
*/
2251+
function test(param) {}
2252+
`,
2253+
options: [
2254+
'always',
2255+
{
2256+
wrapIndent: ' ',
2257+
},
2258+
],
2259+
},
2260+
{
2261+
code: `
2262+
/**
2263+
* Function description.
2264+
*
2265+
* @param {string} lorem Description.
2266+
* @param {int} sit Description with list:
2267+
* - First item
2268+
* - Second item
2269+
* - Nested item
2270+
*/
2271+
const fn = ( lorem, sit ) => {}
2272+
`,
2273+
options: [
2274+
'always',
2275+
{
2276+
wrapIndent: ' ',
2277+
},
2278+
],
2279+
},
2280+
{
2281+
code: `
2282+
/**
2283+
* @return {Promise} A promise.
2284+
* - On success, resolves.
2285+
* - On error, rejects with details:
2286+
* - When aborted, status is "abort".
2287+
* - On timeout, status is "timeout".
2288+
*/
2289+
function test() {}
2290+
`,
2291+
options: [
2292+
'always',
2293+
{
2294+
wrapIndent: ' ',
2295+
},
2296+
],
2297+
},
22432298
],
22442299
});

0 commit comments

Comments
 (0)