eslint-plugin-jsdoc 61.2.0 → 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.
@@ -170,7 +170,8 @@ import esquery from 'esquery';
170
170
  * seedTokens: (
171
171
  * tokens?: Partial<import('comment-parser').Tokens>
172
172
  * ) => import('comment-parser').Tokens,
173
- * descLines: string[]
173
+ * descLines: string[],
174
+ * postDelims: string[]
174
175
  * ) => import('comment-parser').Line[]} setter
175
176
  * @returns {void}
176
177
  */
@@ -257,6 +258,7 @@ import esquery from 'esquery';
257
258
  /**
258
259
  * @callback GetFunctionParameterNames
259
260
  * @param {boolean} [useDefaultObjectProperties]
261
+ * @param {boolean} [ignoreInterfacedParameters]
260
262
  * @returns {import('./jsdocUtils.js').ParamNameInfo[]}
261
263
  */
262
264
 
@@ -927,6 +929,8 @@ const getUtils = (
927
929
  utils.setBlockDescription = (setter) => {
928
930
  /** @type {string[]} */
929
931
  const descLines = [];
932
+ /** @type {string[]} */
933
+ const postDelims = [];
930
934
  /**
931
935
  * @type {undefined|Integer}
932
936
  */
@@ -973,6 +977,7 @@ const getUtils = (
973
977
  return true;
974
978
  }
975
979
 
980
+ postDelims.push(postDelimiter);
976
981
  descLines.push(description);
977
982
  return false;
978
983
  });
@@ -993,6 +998,7 @@ const getUtils = (
993
998
  (info),
994
999
  seedTokens,
995
1000
  descLines,
1001
+ postDelims,
996
1002
  ),
997
1003
  );
998
1004
  }
@@ -1369,8 +1375,8 @@ const getUtils = (
1369
1375
  utils.flattenRoots = jsdocUtils.flattenRoots;
1370
1376
 
1371
1377
  /** @type {GetFunctionParameterNames} */
1372
- utils.getFunctionParameterNames = (useDefaultObjectProperties) => {
1373
- return jsdocUtils.getFunctionParameterNames(node, useDefaultObjectProperties);
1378
+ utils.getFunctionParameterNames = (useDefaultObjectProperties, ignoreInterfacedParameters) => {
1379
+ return jsdocUtils.getFunctionParameterNames(node, useDefaultObjectProperties, ignoreInterfacedParameters);
1374
1380
  };
1375
1381
 
1376
1382
  /** @type {HasParams} */
package/src/jsdocUtils.js CHANGED
@@ -203,11 +203,12 @@ const getPropertiesFromPropertySignature = (propSignature) => {
203
203
  /**
204
204
  * @param {ESTreeOrTypeScriptNode|null} functionNode
205
205
  * @param {boolean} [checkDefaultObjects]
206
+ * @param {boolean} [ignoreInterfacedParameters]
206
207
  * @throws {Error}
207
208
  * @returns {ParamNameInfo[]}
208
209
  */
209
210
  const getFunctionParameterNames = (
210
- functionNode, checkDefaultObjects,
211
+ functionNode, checkDefaultObjects, ignoreInterfacedParameters,
211
212
  ) => {
212
213
  /* eslint-disable complexity -- Temporary */
213
214
  /**
@@ -230,6 +231,19 @@ const getFunctionParameterNames = (
230
231
  const hasLeftTypeAnnotation = 'left' in param && 'typeAnnotation' in param.left;
231
232
 
232
233
  if ('typeAnnotation' in param || hasLeftTypeAnnotation) {
234
+ if (ignoreInterfacedParameters && 'typeAnnotation' in param &&
235
+ param.typeAnnotation) {
236
+ // No-op
237
+ return [
238
+ undefined, {
239
+ hasPropertyRest: false,
240
+ hasRestElement: false,
241
+ names: [],
242
+ rests: [],
243
+ },
244
+ ];
245
+ }
246
+
233
247
  const typeAnnotation = hasLeftTypeAnnotation ?
234
248
  /** @type {import('@typescript-eslint/types').TSESTree.Identifier} */ (
235
249
  param.left
@@ -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']\`.
@@ -65,19 +65,10 @@ export default iterateJsdoc(({
65
65
  useDefaultObjectProperties = false,
66
66
  } = context.options[0] || {};
67
67
 
68
- if (interfaceExemptsParamsCheck) {
69
- if (node && 'params' in node && node.params.length === 1 &&
70
- node.params?.[0] && typeof node.params[0] === 'object' &&
71
- node.params[0].type === 'ObjectPattern' &&
72
- 'typeAnnotation' in node.params[0] && node.params[0].typeAnnotation
73
- ) {
74
- return;
75
- }
76
-
77
- if (node && node.parent?.type === 'VariableDeclarator' &&
78
- 'typeAnnotation' in node.parent.id && node.parent.id.typeAnnotation) {
79
- return;
80
- }
68
+ if (interfaceExemptsParamsCheck && node &&
69
+ node.parent?.type === 'VariableDeclarator' &&
70
+ 'typeAnnotation' in node.parent.id && node.parent.id.typeAnnotation) {
71
+ return;
81
72
  }
82
73
 
83
74
  const preferredTagName = /** @type {string} */ (utils.getPreferredTagName({
@@ -87,7 +78,7 @@ export default iterateJsdoc(({
87
78
  return;
88
79
  }
89
80
 
90
- const functionParameterNames = utils.getFunctionParameterNames(useDefaultObjectProperties);
81
+ const functionParameterNames = utils.getFunctionParameterNames(useDefaultObjectProperties, interfaceExemptsParamsCheck);
91
82
  if (!functionParameterNames.length) {
92
83
  return;
93
84
  }
@@ -37,18 +37,22 @@ const checkMaxBlockLines = ({
37
37
  line: excessIndexLine,
38
38
  },
39
39
  () => {
40
- utils.setBlockDescription((info, seedTokens, descLines) => {
40
+ utils.setBlockDescription((info, seedTokens, descLines, postDelims) => {
41
+ const newPostDelims = [
42
+ ...postDelims.slice(0, excessIndexLine),
43
+ ...postDelims.slice(excessIndexLine + excessBlockLines - 1 - maxBlockLines),
44
+ ];
41
45
  return [
42
46
  ...descLines.slice(0, excessIndexLine),
43
47
  ...descLines.slice(excessIndexLine + excessBlockLines - 1 - maxBlockLines),
44
- ].map((desc) => {
48
+ ].map((desc, idx) => {
45
49
  return {
46
50
  number: 0,
47
51
  source: '',
48
52
  tokens: seedTokens({
49
53
  ...info,
50
54
  description: desc,
51
- postDelimiter: desc.trim() ? ' ' : '',
55
+ postDelimiter: newPostDelims[idx],
52
56
  }),
53
57
  };
54
58
  });
@@ -310,15 +314,15 @@ export default iterateJsdoc(({
310
314
  line: lastDescriptionLine - trailingDiff,
311
315
  },
312
316
  () => {
313
- utils.setBlockDescription((info, seedTokens, descLines) => {
314
- return descLines.slice(0, -trailingDiff).map((desc) => {
317
+ utils.setBlockDescription((info, seedTokens, descLines, postDelims) => {
318
+ return descLines.slice(0, -trailingDiff).map((desc, idx) => {
315
319
  return {
316
320
  number: 0,
317
321
  source: '',
318
322
  tokens: seedTokens({
319
323
  ...info,
320
324
  description: desc,
321
- postDelimiter: desc.trim() ? info.postDelimiter : '',
325
+ postDelimiter: postDelims[idx],
322
326
  }),
323
327
  };
324
328
  });
@@ -332,7 +336,7 @@ export default iterateJsdoc(({
332
336
  line: lastDescriptionLine,
333
337
  },
334
338
  () => {
335
- utils.setBlockDescription((info, seedTokens, descLines) => {
339
+ utils.setBlockDescription((info, seedTokens, descLines, postDelims) => {
336
340
  return [
337
341
  ...descLines,
338
342
  ...Array.from({
@@ -340,14 +344,14 @@ export default iterateJsdoc(({
340
344
  }, () => {
341
345
  return '';
342
346
  }),
343
- ].map((desc) => {
347
+ ].map((desc, idx) => {
344
348
  return {
345
349
  number: 0,
346
350
  source: '',
347
351
  tokens: seedTokens({
348
352
  ...info,
349
353
  description: desc,
350
- postDelimiter: desc.trim() ? info.postDelimiter : '',
354
+ postDelimiter: desc.trim() ? postDelims[idx] : '',
351
355
  }),
352
356
  };
353
357
  });
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']`.