diff --git a/.README/rules/check-indentation.md b/.README/rules/check-indentation.md
index 805cc5f04..9e0c4777e 100644
--- a/.README/rules/check-indentation.md
+++ b/.README/rules/check-indentation.md
@@ -29,7 +29,7 @@ the following description is not reported:
|Context|everywhere|
|Tags|N/A|
|Recommended|false|
-|Options|`excludeTags`|
+|Options|`allowIndentedSections`, `excludeTags`|
## Failing examples
diff --git a/docs/rules/check-indentation.md b/docs/rules/check-indentation.md
index a692d8a38..a72efc131 100644
--- a/docs/rules/check-indentation.md
+++ b/docs/rules/check-indentation.md
@@ -3,6 +3,7 @@
# check-indentation
* [Options](#user-content-check-indentation-options)
+ * [`allowIndentedSections`](#user-content-check-indentation-options-allowindentedsections)
* [`excludeTags`](#user-content-check-indentation-options-excludetags)
* [Context and settings](#user-content-check-indentation-context-and-settings)
* [Failing examples](#user-content-check-indentation-failing-examples)
@@ -31,6 +32,12 @@ the following description is not reported:
A single options object has the following properties.
+
+
+### allowIndentedSections
+
+Allows indentation of nested sections on subsequent lines (like bullet lists)
+
### excludeTags
@@ -65,7 +72,7 @@ report a padding issue:
|Context|everywhere|
|Tags|N/A|
|Recommended|false|
-|Options|`excludeTags`|
+|Options|`allowIndentedSections`, `excludeTags`|
@@ -179,6 +186,32 @@ function quux () {
*/
// "jsdoc/check-indentation": ["error"|"warn", {"excludeTags":[]}]
// Message: There must be no indentation.
+
+/**
+ * @param {number} val Still disallowed
+ */
+// "jsdoc/check-indentation": ["error"|"warn", {"allowIndentedSections":true}]
+// Message: There must be no indentation.
+
+/**
+ * Disallowed
+ * Indentation
+ */
+// "jsdoc/check-indentation": ["error"|"warn", {"allowIndentedSections":true}]
+// Message: There must be no indentation.
+
+/**
+ * Some text
+ * that is indented
+ * but is inconsistent
+ */
+// "jsdoc/check-indentation": ["error"|"warn", {"allowIndentedSections":true}]
+// Message: There must be no indentation.
+
+/** Indented on first line
+ */
+// "jsdoc/check-indentation": ["error"|"warn", {"allowIndentedSections":true}]
+// Message: There must be no indentation.
````
@@ -293,5 +326,57 @@ function MyDecorator(options: { myOptions: number }) {
function MyDecorator(options: { myOptions: number }) {
return (Base: Function) => {};
}
+
+/**
+ * Foobar
+ *
+ * This method does the following things:
+ * - foo...
+ * this is the first step
+ * - bar
+ * this is the second step
+ */
+// "jsdoc/check-indentation": ["error"|"warn", {"allowIndentedSections":true}]
+
+/**
+ * Allowed
+ * Indentation
+ */
+// "jsdoc/check-indentation": ["error"|"warn", {"allowIndentedSections":true}]
+
+/**
+ * @param {number} val Multi-
+ * line
+ */
+// "jsdoc/check-indentation": ["error"|"warn", {"allowIndentedSections":true}]
+
+/**
+ * - foo:
+ * - bar
+ */
+// "jsdoc/check-indentation": ["error"|"warn", {"allowIndentedSections":true}]
+
+/**
+ * Some text
+ * that is indented
+ * and continues at same level
+ * and increases further
+ */
+// "jsdoc/check-indentation": ["error"|"warn", {"allowIndentedSections":true}]
+
+/**
+ * Description
+ * @param {string} foo Param
+ * with continuation
+ * at same indentation
+ */
+// "jsdoc/check-indentation": ["error"|"warn", {"allowIndentedSections":true}]
+
+/**
+ * Description
+ *
+ * More content
+ */
+// "jsdoc/check-indentation": ["error"|"warn", {"allowIndentedSections":true}]
````
diff --git a/src/rules.d.ts b/src/rules.d.ts
index 7a280390e..3ee700908 100644
--- a/src/rules.d.ts
+++ b/src/rules.d.ts
@@ -47,6 +47,10 @@ export interface Rules {
| []
| [
{
+ /**
+ * Allows indentation of nested sections on subsequent lines (like bullet lists)
+ */
+ allowIndentedSections?: boolean;
/**
* Array of tags (e.g., `['example', 'description']`) whose content will be
* "hidden" from the `check-indentation` rule. Defaults to `['example']`.
diff --git a/src/rules/checkIndentation.js b/src/rules/checkIndentation.js
index d8bb80f68..6e6f2b9bc 100644
--- a/src/rules/checkIndentation.js
+++ b/src/rules/checkIndentation.js
@@ -25,6 +25,17 @@ const maskCodeBlocks = (str) => {
});
};
+/**
+ * @param {string[]} lines
+ * @param {number} lineIndex
+ * @returns {number}
+ */
+const getLineNumber = (lines, lineIndex) => {
+ const precedingText = lines.slice(0, lineIndex).join('\n');
+ const lineBreaks = precedingText.match(/\n/gv) || [];
+ return lineBreaks.length + 1;
+};
+
export default iterateJsdoc(({
context,
jsdocNode,
@@ -32,21 +43,88 @@ export default iterateJsdoc(({
sourceCode,
}) => {
const options = context.options[0] || {};
- const /** @type {{excludeTags: string[]}} */ {
+ const /** @type {{excludeTags: string[], allowIndentedSections: boolean}} */ {
+ allowIndentedSections = false,
excludeTags = [
'example',
],
} = options;
- const reg = /^(?:\/?\**|[ \t]*)\*[ \t]{2}/gmv;
const textWithoutCodeBlocks = maskCodeBlocks(sourceCode.getText(jsdocNode));
const text = excludeTags.length ? maskExcludedContent(textWithoutCodeBlocks, excludeTags) : textWithoutCodeBlocks;
- if (reg.test(text)) {
- const lineBreaks = text.slice(0, reg.lastIndex).match(/\n/gv) || [];
- report('There must be no indentation.', null, {
- line: lineBreaks.length,
- });
+ if (allowIndentedSections) {
+ // When allowIndentedSections is enabled, only check for indentation on tag lines
+ // and the very first line of the main description
+ const lines = text.split('\n');
+ let hasSeenContent = false;
+ let currentSectionIndent = null;
+
+ for (const [
+ lineIndex,
+ line,
+ ] of lines.entries()) {
+ // Check for indentation (two or more spaces after *)
+ const indentMatch = line.match(/^(?:\/?\**|[\t ]*)\*([\t ]{2,})/v);
+
+ if (indentMatch) {
+ // Check what comes after the indentation
+ const afterIndent = line.slice(indentMatch[0].length);
+ const indentAmount = indentMatch[1].length;
+
+ // If this is a tag line with indentation, always report
+ if (/^@\w+/v.test(afterIndent)) {
+ report('There must be no indentation.', null, {
+ line: getLineNumber(lines, lineIndex),
+ });
+ return;
+ }
+
+ // If we haven't seen any content yet (main description first line) and there's content, report
+ if (!hasSeenContent && afterIndent.trim().length > 0) {
+ report('There must be no indentation.', null, {
+ line: getLineNumber(lines, lineIndex),
+ });
+ return;
+ }
+
+ // For continuation lines, check consistency
+ if (hasSeenContent && afterIndent.trim().length > 0) {
+ if (currentSectionIndent === null) {
+ // First indented line in this section, set the indent level
+ currentSectionIndent = indentAmount;
+ } else if (indentAmount < currentSectionIndent) {
+ // Indentation is less than the established level (inconsistent)
+ report('There must be no indentation.', null, {
+ line: getLineNumber(lines, lineIndex),
+ });
+ return;
+ }
+ }
+ } else if (/^\s*\*\s+\S/v.test(line)) {
+ // No indentation on this line, reset section indent tracking
+ // (unless it's just whitespace or a closing comment)
+ currentSectionIndent = null;
+ }
+
+ // Track if we've seen any content (non-whitespace after the *)
+ if (/^\s*\*\s+\S/v.test(line)) {
+ hasSeenContent = true;
+ }
+
+ // Reset section indent when we encounter a tag
+ if (/@\w+/v.test(line)) {
+ currentSectionIndent = null;
+ }
+ }
+ } else {
+ const reg = /^(?:\/?\**|[ \t]*)\*[ \t]{2}/gmv;
+ if (reg.test(text)) {
+ const lineBreaks = text.slice(0, reg.lastIndex).match(/\n/gv) || [];
+ report('There must be no indentation.', null, {
+ line: lineBreaks.length,
+ });
+ }
}
}, {
iterateAllJsdocs: true,
@@ -59,6 +137,10 @@ export default iterateJsdoc(({
{
additionalProperties: false,
properties: {
+ allowIndentedSections: {
+ description: 'Allows indentation of nested sections on subsequent lines (like bullet lists)',
+ type: 'boolean',
+ },
excludeTags: {
description: `Array of tags (e.g., \`['example', 'description']\`) whose content will be
"hidden" from the \`check-indentation\` rule. Defaults to \`['example']\`.
diff --git a/test/rules/assertions/checkIndentation.js b/test/rules/assertions/checkIndentation.js
index 90c8cc8ce..7521bafc4 100644
--- a/test/rules/assertions/checkIndentation.js
+++ b/test/rules/assertions/checkIndentation.js
@@ -194,6 +194,80 @@ export default /** @type {import('../index.js').TestCases} */ ({
},
],
},
+ {
+ code: `
+ /**
+ * @param {number} val Still disallowed
+ */
+ `,
+ errors: [
+ {
+ line: 3,
+ message: 'There must be no indentation.',
+ },
+ ],
+ options: [
+ {
+ allowIndentedSections: true,
+ },
+ ],
+ },
+ {
+ code: `
+ /**
+ * Disallowed
+ * Indentation
+ */
+ `,
+ errors: [
+ {
+ line: 3,
+ message: 'There must be no indentation.',
+ },
+ ],
+ options: [
+ {
+ allowIndentedSections: true,
+ },
+ ],
+ },
+ {
+ code: `
+ /**
+ * Some text
+ * that is indented
+ * but is inconsistent
+ */
+ `,
+ errors: [
+ {
+ line: 5,
+ message: 'There must be no indentation.',
+ },
+ ],
+ options: [
+ {
+ allowIndentedSections: true,
+ },
+ ],
+ },
+ {
+ code: `
+ /** Indented on first line
+ */
+ `,
+ errors: [
+ {
+ line: 3,
+ message: 'There must be no indentation.',
+ },
+ ],
+ options: [
+ {
+ allowIndentedSections: true,
+ },
+ ],
+ },
],
valid: [
{
@@ -349,5 +423,106 @@ export default /** @type {import('../index.js').TestCases} */ ({
parser: typescriptEslintParser,
},
},
+ {
+ code: `
+ /**
+ * Foobar
+ *
+ * This method does the following things:
+ * - foo...
+ * this is the first step
+ * - bar
+ * this is the second step
+ */
+ `,
+ options: [
+ {
+ allowIndentedSections: true,
+ },
+ ],
+ },
+ {
+ code: `
+ /**
+ * Allowed
+ * Indentation
+ */
+ `,
+ options: [
+ {
+ allowIndentedSections: true,
+ },
+ ],
+ },
+ {
+ code: `
+ /**
+ * @param {number} val Multi-
+ * line
+ */
+ `,
+ options: [
+ {
+ allowIndentedSections: true,
+ },
+ ],
+ },
+ {
+ code: `
+ /**
+ * - foo:
+ * - bar
+ */
+ `,
+ options: [
+ {
+ allowIndentedSections: true,
+ },
+ ],
+ },
+ {
+ code: `
+ /**
+ * Some text
+ * that is indented
+ * and continues at same level
+ * and increases further
+ */
+ `,
+ options: [
+ {
+ allowIndentedSections: true,
+ },
+ ],
+ },
+ {
+ code: `
+ /**
+ * Description
+ * @param {string} foo Param
+ * with continuation
+ * at same indentation
+ */
+ `,
+ options: [
+ {
+ allowIndentedSections: true,
+ },
+ ],
+ },
+ {
+ code: `
+ /**
+ * Description
+ *
+ * More content
+ */
+ `,
+ options: [
+ {
+ allowIndentedSections: true,
+ },
+ ],
+ },
],
});