eslint-plugin-jsdoc 61.2.1 → 61.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -28,6 +28,17 @@ const maskCodeBlocks = str => {
28
28
  return (margin + '\n').repeat(code.match(/\n/gv).length);
29
29
  });
30
30
  };
31
+
32
+ /**
33
+ * @param {string[]} lines
34
+ * @param {number} lineIndex
35
+ * @returns {number}
36
+ */
37
+ const getLineNumber = (lines, lineIndex) => {
38
+ const precedingText = lines.slice(0, lineIndex).join('\n');
39
+ const lineBreaks = precedingText.match(/\n/gv) || [];
40
+ return lineBreaks.length + 1;
41
+ };
31
42
  var _default = exports.default = (0, _iterateJsdoc.default)(({
32
43
  context,
33
44
  jsdocNode,
@@ -35,17 +46,79 @@ var _default = exports.default = (0, _iterateJsdoc.default)(({
35
46
  sourceCode
36
47
  }) => {
37
48
  const options = context.options[0] || {};
38
- const /** @type {{excludeTags: string[]}} */{
49
+ const /** @type {{excludeTags: string[], allowIndentedSections: boolean}} */{
50
+ allowIndentedSections = false,
39
51
  excludeTags = ['example']
40
52
  } = options;
41
- const reg = /^(?:\/?\**|[ \t]*)\*[ \t]{2}/gmv;
42
53
  const textWithoutCodeBlocks = maskCodeBlocks(sourceCode.getText(jsdocNode));
43
54
  const text = excludeTags.length ? maskExcludedContent(textWithoutCodeBlocks, excludeTags) : textWithoutCodeBlocks;
44
- if (reg.test(text)) {
45
- const lineBreaks = text.slice(0, reg.lastIndex).match(/\n/gv) || [];
46
- report('There must be no indentation.', null, {
47
- line: lineBreaks.length
48
- });
55
+ if (allowIndentedSections) {
56
+ // When allowIndentedSections is enabled, only check for indentation on tag lines
57
+ // and the very first line of the main description
58
+ const lines = text.split('\n');
59
+ let hasSeenContent = false;
60
+ let currentSectionIndent = null;
61
+ for (const [lineIndex, line] of lines.entries()) {
62
+ // Check for indentation (two or more spaces after *)
63
+ const indentMatch = line.match(/^(?:\/?\**|[\t ]*)\*([\t ]{2,})/v);
64
+ if (indentMatch) {
65
+ // Check what comes after the indentation
66
+ const afterIndent = line.slice(indentMatch[0].length);
67
+ const indentAmount = indentMatch[1].length;
68
+
69
+ // If this is a tag line with indentation, always report
70
+ if (/^@\w+/v.test(afterIndent)) {
71
+ report('There must be no indentation.', null, {
72
+ line: getLineNumber(lines, lineIndex)
73
+ });
74
+ return;
75
+ }
76
+
77
+ // If we haven't seen any content yet (main description first line) and there's content, report
78
+ if (!hasSeenContent && afterIndent.trim().length > 0) {
79
+ report('There must be no indentation.', null, {
80
+ line: getLineNumber(lines, lineIndex)
81
+ });
82
+ return;
83
+ }
84
+
85
+ // For continuation lines, check consistency
86
+ if (hasSeenContent && afterIndent.trim().length > 0) {
87
+ if (currentSectionIndent === null) {
88
+ // First indented line in this section, set the indent level
89
+ currentSectionIndent = indentAmount;
90
+ } else if (indentAmount < currentSectionIndent) {
91
+ // Indentation is less than the established level (inconsistent)
92
+ report('There must be no indentation.', null, {
93
+ line: getLineNumber(lines, lineIndex)
94
+ });
95
+ return;
96
+ }
97
+ }
98
+ } else if (/^\s*\*\s+\S/v.test(line)) {
99
+ // No indentation on this line, reset section indent tracking
100
+ // (unless it's just whitespace or a closing comment)
101
+ currentSectionIndent = null;
102
+ }
103
+
104
+ // Track if we've seen any content (non-whitespace after the *)
105
+ if (/^\s*\*\s+\S/v.test(line)) {
106
+ hasSeenContent = true;
107
+ }
108
+
109
+ // Reset section indent when we encounter a tag
110
+ if (/@\w+/v.test(line)) {
111
+ currentSectionIndent = null;
112
+ }
113
+ }
114
+ } else {
115
+ const reg = /^(?:\/?\**|[ \t]*)\*[ \t]{2}/gmv;
116
+ if (reg.test(text)) {
117
+ const lineBreaks = text.slice(0, reg.lastIndex).match(/\n/gv) || [];
118
+ report('There must be no indentation.', null, {
119
+ line: lineBreaks.length
120
+ });
121
+ }
49
122
  }
50
123
  }, {
51
124
  iterateAllJsdocs: true,
@@ -57,6 +130,10 @@ var _default = exports.default = (0, _iterateJsdoc.default)(({
57
130
  schema: [{
58
131
  additionalProperties: false,
59
132
  properties: {
133
+ allowIndentedSections: {
134
+ description: 'Allows indentation of nested sections on subsequent lines (like bullet lists)',
135
+ type: 'boolean'
136
+ },
60
137
  excludeTags: {
61
138
  description: `Array of tags (e.g., \`['example', 'description']\`) whose content will be
62
139
  "hidden" from the \`check-indentation\` rule. Defaults to \`['example']\`.
@@ -1 +1 @@
1
- {"version":3,"file":"checkIndentation.cjs","names":["_iterateJsdoc","_interopRequireDefault","require","e","__esModule","default","maskExcludedContent","str","excludeTags","regContent","RegExp","join","replace","_match","margin","code","repeat","match","length","maskCodeBlocks","replaceAll","_default","exports","iterateJsdoc","context","jsdocNode","report","sourceCode","options","reg","textWithoutCodeBlocks","getText","text","test","lineBreaks","slice","lastIndex","line","iterateAllJsdocs","meta","docs","description","url","schema","additionalProperties","properties","items","pattern","type","module"],"sources":["../../src/rules/checkIndentation.js"],"sourcesContent":["import iterateJsdoc from '../iterateJsdoc.js';\n\n/**\n * @param {string} str\n * @param {string[]} excludeTags\n * @returns {string}\n */\nconst maskExcludedContent = (str, excludeTags) => {\n const regContent = new RegExp(`([ \\\\t]+\\\\*)[ \\\\t]@(?:${excludeTags.join('|')})(?=[ \\\\n])([\\\\w\\\\|\\\\W]*?\\\\n)(?=[ \\\\t]*\\\\*(?:[ \\\\t]*@\\\\w+\\\\s|\\\\/))`, 'gv');\n\n return str.replace(regContent, (_match, margin, code) => {\n return (margin + '\\n').repeat(code.match(/\\n/gv).length);\n });\n};\n\n/**\n * @param {string} str\n * @returns {string}\n */\nconst maskCodeBlocks = (str) => {\n const regContent = /([ \\t]+\\*)[ \\t]```[^\\n]*?([\\w\\|\\W]*?\\n)(?=[ \\t]*\\*(?:[ \\t]*(?:```|@\\w+\\s)|\\/))/gv;\n\n return str.replaceAll(regContent, (_match, margin, code) => {\n return (margin + '\\n').repeat(code.match(/\\n/gv).length);\n });\n};\n\nexport default iterateJsdoc(({\n context,\n jsdocNode,\n report,\n sourceCode,\n}) => {\n const options = context.options[0] || {};\n const /** @type {{excludeTags: string[]}} */ {\n excludeTags = [\n 'example',\n ],\n } = options;\n\n const reg = /^(?:\\/?\\**|[ \\t]*)\\*[ \\t]{2}/gmv;\n const textWithoutCodeBlocks = maskCodeBlocks(sourceCode.getText(jsdocNode));\n const text = excludeTags.length ? maskExcludedContent(textWithoutCodeBlocks, excludeTags) : textWithoutCodeBlocks;\n\n if (reg.test(text)) {\n const lineBreaks = text.slice(0, reg.lastIndex).match(/\\n/gv) || [];\n report('There must be no indentation.', null, {\n line: lineBreaks.length,\n });\n }\n}, {\n iterateAllJsdocs: true,\n meta: {\n docs: {\n description: 'Reports invalid padding inside JSDoc blocks.',\n url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/check-indentation.md#repos-sticky-header',\n },\n schema: [\n {\n additionalProperties: false,\n properties: {\n excludeTags: {\n description: `Array of tags (e.g., \\`['example', 'description']\\`) whose content will be\n\"hidden\" from the \\`check-indentation\\` rule. Defaults to \\`['example']\\`.\n\nBy default, the whole JSDoc block will be checked for invalid padding.\nThat would include \\`@example\\` blocks too, which can get in the way\nof adding full, readable examples of code without ending up with multiple\nlinting issues.\n\nWhen disabled (by passing \\`excludeTags: []\\` option), the following code *will*\nreport a padding issue:\n\n\\`\\`\\`js\n/**\n * @example\n * anArray.filter((a) => {\n * return a.b;\n * });\n */\n\\`\\`\\``,\n items: {\n pattern: '^\\\\S+$',\n type: 'string',\n },\n type: 'array',\n },\n },\n type: 'object',\n },\n ],\n type: 'layout',\n },\n});\n"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"file":"checkIndentation.cjs","names":["_iterateJsdoc","_interopRequireDefault","require","e","__esModule","default","maskExcludedContent","str","excludeTags","regContent","RegExp","join","replace","_match","margin","code","repeat","match","length","maskCodeBlocks","replaceAll","getLineNumber","lines","lineIndex","precedingText","slice","lineBreaks","_default","exports","iterateJsdoc","context","jsdocNode","report","sourceCode","options","allowIndentedSections","textWithoutCodeBlocks","getText","text","split","hasSeenContent","currentSectionIndent","line","entries","indentMatch","afterIndent","indentAmount","test","trim","reg","lastIndex","iterateAllJsdocs","meta","docs","description","url","schema","additionalProperties","properties","type","items","pattern","module"],"sources":["../../src/rules/checkIndentation.js"],"sourcesContent":["import iterateJsdoc from '../iterateJsdoc.js';\n\n/**\n * @param {string} str\n * @param {string[]} excludeTags\n * @returns {string}\n */\nconst maskExcludedContent = (str, excludeTags) => {\n const regContent = new RegExp(`([ \\\\t]+\\\\*)[ \\\\t]@(?:${excludeTags.join('|')})(?=[ \\\\n])([\\\\w\\\\|\\\\W]*?\\\\n)(?=[ \\\\t]*\\\\*(?:[ \\\\t]*@\\\\w+\\\\s|\\\\/))`, 'gv');\n\n return str.replace(regContent, (_match, margin, code) => {\n return (margin + '\\n').repeat(code.match(/\\n/gv).length);\n });\n};\n\n/**\n * @param {string} str\n * @returns {string}\n */\nconst maskCodeBlocks = (str) => {\n const regContent = /([ \\t]+\\*)[ \\t]```[^\\n]*?([\\w\\|\\W]*?\\n)(?=[ \\t]*\\*(?:[ \\t]*(?:```|@\\w+\\s)|\\/))/gv;\n\n return str.replaceAll(regContent, (_match, margin, code) => {\n return (margin + '\\n').repeat(code.match(/\\n/gv).length);\n });\n};\n\n/**\n * @param {string[]} lines\n * @param {number} lineIndex\n * @returns {number}\n */\nconst getLineNumber = (lines, lineIndex) => {\n const precedingText = lines.slice(0, lineIndex).join('\\n');\n const lineBreaks = precedingText.match(/\\n/gv) || [];\n return lineBreaks.length + 1;\n};\n\nexport default iterateJsdoc(({\n context,\n jsdocNode,\n report,\n sourceCode,\n}) => {\n const options = context.options[0] || {};\n const /** @type {{excludeTags: string[], allowIndentedSections: boolean}} */ {\n allowIndentedSections = false,\n excludeTags = [\n 'example',\n ],\n } = options;\n\n const textWithoutCodeBlocks = maskCodeBlocks(sourceCode.getText(jsdocNode));\n const text = excludeTags.length ? maskExcludedContent(textWithoutCodeBlocks, excludeTags) : textWithoutCodeBlocks;\n\n if (allowIndentedSections) {\n // When allowIndentedSections is enabled, only check for indentation on tag lines\n // and the very first line of the main description\n const lines = text.split('\\n');\n let hasSeenContent = false;\n let currentSectionIndent = null;\n\n for (const [\n lineIndex,\n line,\n ] of lines.entries()) {\n // Check for indentation (two or more spaces after *)\n const indentMatch = line.match(/^(?:\\/?\\**|[\\t ]*)\\*([\\t ]{2,})/v);\n\n if (indentMatch) {\n // Check what comes after the indentation\n const afterIndent = line.slice(indentMatch[0].length);\n const indentAmount = indentMatch[1].length;\n\n // If this is a tag line with indentation, always report\n if (/^@\\w+/v.test(afterIndent)) {\n report('There must be no indentation.', null, {\n line: getLineNumber(lines, lineIndex),\n });\n return;\n }\n\n // If we haven't seen any content yet (main description first line) and there's content, report\n if (!hasSeenContent && afterIndent.trim().length > 0) {\n report('There must be no indentation.', null, {\n line: getLineNumber(lines, lineIndex),\n });\n return;\n }\n\n // For continuation lines, check consistency\n if (hasSeenContent && afterIndent.trim().length > 0) {\n if (currentSectionIndent === null) {\n // First indented line in this section, set the indent level\n currentSectionIndent = indentAmount;\n } else if (indentAmount < currentSectionIndent) {\n // Indentation is less than the established level (inconsistent)\n report('There must be no indentation.', null, {\n line: getLineNumber(lines, lineIndex),\n });\n return;\n }\n }\n } else if (/^\\s*\\*\\s+\\S/v.test(line)) {\n // No indentation on this line, reset section indent tracking\n // (unless it's just whitespace or a closing comment)\n currentSectionIndent = null;\n }\n\n // Track if we've seen any content (non-whitespace after the *)\n if (/^\\s*\\*\\s+\\S/v.test(line)) {\n hasSeenContent = true;\n }\n\n // Reset section indent when we encounter a tag\n if (/@\\w+/v.test(line)) {\n currentSectionIndent = null;\n }\n }\n } else {\n const reg = /^(?:\\/?\\**|[ \\t]*)\\*[ \\t]{2}/gmv;\n if (reg.test(text)) {\n const lineBreaks = text.slice(0, reg.lastIndex).match(/\\n/gv) || [];\n report('There must be no indentation.', null, {\n line: lineBreaks.length,\n });\n }\n }\n}, {\n iterateAllJsdocs: true,\n meta: {\n docs: {\n description: 'Reports invalid padding inside JSDoc blocks.',\n url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/check-indentation.md#repos-sticky-header',\n },\n schema: [\n {\n additionalProperties: false,\n properties: {\n allowIndentedSections: {\n description: 'Allows indentation of nested sections on subsequent lines (like bullet lists)',\n type: 'boolean',\n },\n excludeTags: {\n description: `Array of tags (e.g., \\`['example', 'description']\\`) whose content will be\n\"hidden\" from the \\`check-indentation\\` rule. Defaults to \\`['example']\\`.\n\nBy default, the whole JSDoc block will be checked for invalid padding.\nThat would include \\`@example\\` blocks too, which can get in the way\nof adding full, readable examples of code without ending up with multiple\nlinting issues.\n\nWhen disabled (by passing \\`excludeTags: []\\` option), the following code *will*\nreport a padding issue:\n\n\\`\\`\\`js\n/**\n * @example\n * anArray.filter((a) => {\n * return a.b;\n * });\n */\n\\`\\`\\``,\n items: {\n pattern: '^\\\\S+$',\n type: 'string',\n },\n type: 'array',\n },\n },\n type: 'object',\n },\n ],\n type: 'layout',\n },\n});\n"],"mappings":"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","ignoreList":[]}
package/dist/rules.d.ts CHANGED
@@ -47,6 +47,10 @@ export interface Rules {
47
47
  | []
48
48
  | [
49
49
  {
50
+ /**
51
+ * Allows indentation of nested sections on subsequent lines (like bullet lists)
52
+ */
53
+ allowIndentedSections?: boolean;
50
54
  /**
51
55
  * Array of tags (e.g., `['example', 'description']`) whose content will be
52
56
  * "hidden" from the `check-indentation` rule. Defaults to `['example']`.
package/package.json CHANGED
@@ -192,5 +192,5 @@
192
192
  "test-cov": "TIMING=1 c8 --reporter text pnpm run test-no-cov",
193
193
  "test-index": "pnpm run test-no-cov test/rules/index.js"
194
194
  },
195
- "version": "61.2.1"
195
+ "version": "61.3.0"
196
196
  }
@@ -25,6 +25,17 @@ const maskCodeBlocks = (str) => {
25
25
  });
26
26
  };
27
27
 
28
+ /**
29
+ * @param {string[]} lines
30
+ * @param {number} lineIndex
31
+ * @returns {number}
32
+ */
33
+ const getLineNumber = (lines, lineIndex) => {
34
+ const precedingText = lines.slice(0, lineIndex).join('\n');
35
+ const lineBreaks = precedingText.match(/\n/gv) || [];
36
+ return lineBreaks.length + 1;
37
+ };
38
+
28
39
  export default iterateJsdoc(({
29
40
  context,
30
41
  jsdocNode,
@@ -32,21 +43,88 @@ export default iterateJsdoc(({
32
43
  sourceCode,
33
44
  }) => {
34
45
  const options = context.options[0] || {};
35
- const /** @type {{excludeTags: string[]}} */ {
46
+ const /** @type {{excludeTags: string[], allowIndentedSections: boolean}} */ {
47
+ allowIndentedSections = false,
36
48
  excludeTags = [
37
49
  'example',
38
50
  ],
39
51
  } = options;
40
52
 
41
- const reg = /^(?:\/?\**|[ \t]*)\*[ \t]{2}/gmv;
42
53
  const textWithoutCodeBlocks = maskCodeBlocks(sourceCode.getText(jsdocNode));
43
54
  const text = excludeTags.length ? maskExcludedContent(textWithoutCodeBlocks, excludeTags) : textWithoutCodeBlocks;
44
55
 
45
- if (reg.test(text)) {
46
- const lineBreaks = text.slice(0, reg.lastIndex).match(/\n/gv) || [];
47
- report('There must be no indentation.', null, {
48
- line: lineBreaks.length,
49
- });
56
+ if (allowIndentedSections) {
57
+ // When allowIndentedSections is enabled, only check for indentation on tag lines
58
+ // and the very first line of the main description
59
+ const lines = text.split('\n');
60
+ let hasSeenContent = false;
61
+ let currentSectionIndent = null;
62
+
63
+ for (const [
64
+ lineIndex,
65
+ line,
66
+ ] of lines.entries()) {
67
+ // Check for indentation (two or more spaces after *)
68
+ const indentMatch = line.match(/^(?:\/?\**|[\t ]*)\*([\t ]{2,})/v);
69
+
70
+ if (indentMatch) {
71
+ // Check what comes after the indentation
72
+ const afterIndent = line.slice(indentMatch[0].length);
73
+ const indentAmount = indentMatch[1].length;
74
+
75
+ // If this is a tag line with indentation, always report
76
+ if (/^@\w+/v.test(afterIndent)) {
77
+ report('There must be no indentation.', null, {
78
+ line: getLineNumber(lines, lineIndex),
79
+ });
80
+ return;
81
+ }
82
+
83
+ // If we haven't seen any content yet (main description first line) and there's content, report
84
+ if (!hasSeenContent && afterIndent.trim().length > 0) {
85
+ report('There must be no indentation.', null, {
86
+ line: getLineNumber(lines, lineIndex),
87
+ });
88
+ return;
89
+ }
90
+
91
+ // For continuation lines, check consistency
92
+ if (hasSeenContent && afterIndent.trim().length > 0) {
93
+ if (currentSectionIndent === null) {
94
+ // First indented line in this section, set the indent level
95
+ currentSectionIndent = indentAmount;
96
+ } else if (indentAmount < currentSectionIndent) {
97
+ // Indentation is less than the established level (inconsistent)
98
+ report('There must be no indentation.', null, {
99
+ line: getLineNumber(lines, lineIndex),
100
+ });
101
+ return;
102
+ }
103
+ }
104
+ } else if (/^\s*\*\s+\S/v.test(line)) {
105
+ // No indentation on this line, reset section indent tracking
106
+ // (unless it's just whitespace or a closing comment)
107
+ currentSectionIndent = null;
108
+ }
109
+
110
+ // Track if we've seen any content (non-whitespace after the *)
111
+ if (/^\s*\*\s+\S/v.test(line)) {
112
+ hasSeenContent = true;
113
+ }
114
+
115
+ // Reset section indent when we encounter a tag
116
+ if (/@\w+/v.test(line)) {
117
+ currentSectionIndent = null;
118
+ }
119
+ }
120
+ } else {
121
+ const reg = /^(?:\/?\**|[ \t]*)\*[ \t]{2}/gmv;
122
+ if (reg.test(text)) {
123
+ const lineBreaks = text.slice(0, reg.lastIndex).match(/\n/gv) || [];
124
+ report('There must be no indentation.', null, {
125
+ line: lineBreaks.length,
126
+ });
127
+ }
50
128
  }
51
129
  }, {
52
130
  iterateAllJsdocs: true,
@@ -59,6 +137,10 @@ export default iterateJsdoc(({
59
137
  {
60
138
  additionalProperties: false,
61
139
  properties: {
140
+ allowIndentedSections: {
141
+ description: 'Allows indentation of nested sections on subsequent lines (like bullet lists)',
142
+ type: 'boolean',
143
+ },
62
144
  excludeTags: {
63
145
  description: `Array of tags (e.g., \`['example', 'description']\`) whose content will be
64
146
  "hidden" from the \`check-indentation\` rule. Defaults to \`['example']\`.
package/src/rules.d.ts CHANGED
@@ -47,6 +47,10 @@ export interface Rules {
47
47
  | []
48
48
  | [
49
49
  {
50
+ /**
51
+ * Allows indentation of nested sections on subsequent lines (like bullet lists)
52
+ */
53
+ allowIndentedSections?: boolean;
50
54
  /**
51
55
  * Array of tags (e.g., `['example', 'description']`) whose content will be
52
56
  * "hidden" from the `check-indentation` rule. Defaults to `['example']`.