eslint-plugin-jsdoc 61.3.0 → 61.4.1

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.
@@ -8,6 +8,54 @@ const {
8
8
  flow: commentFlow,
9
9
  } = transforms;
10
10
 
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
+
11
59
  /**
12
60
  * @typedef {{
13
61
  * postDelimiter: import('../iterateJsdoc.js').Integer,
@@ -298,7 +346,17 @@ export default iterateJsdoc(({
298
346
  }
299
347
 
300
348
  // 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)) {
302
360
  utils.reportJSDoc('Expected wrap indent', {
303
361
  line: tag.source[0].number + idx,
304
362
  }, () => {
@@ -124,7 +124,8 @@ export default iterateJsdoc(({
124
124
  */
125
125
  const isInAmbientContext = (subNode) => {
126
126
  return subNode.type === 'Program' ?
127
- context.getFilename().endsWith('.d.ts') :
127
+ /* c8 ignore next -- Support old ESLint */
128
+ (context.filename ?? context.getFilename()).endsWith('.d.ts') :
128
129
  Boolean(
129
130
  /** @type {import('@typescript-eslint/types').TSESTree.VariableDeclaration} */ (
130
131
  subNode
@@ -149,7 +150,8 @@ export default iterateJsdoc(({
149
150
  return false;
150
151
  }
151
152
 
152
- if (context.getFilename().endsWith('.d.ts') && [
153
+ /* c8 ignore next -- Support old ESLint */
154
+ if ((context.filename ?? context.getFilename()).endsWith('.d.ts') && [
153
155
  null, 'Program', undefined,
154
156
  ].includes(node?.parent?.type)) {
155
157
  return false;
@@ -0,0 +1,246 @@
1
+ import iterateJsdoc from '../iterateJsdoc.js';
2
+
3
+ /**
4
+ * Checks if a node or its children contain Promise rejection patterns
5
+ * @param {import('eslint').Rule.Node} node
6
+ * @param {boolean} [innerFunction]
7
+ * @param {boolean} [isAsync]
8
+ * @returns {boolean}
9
+ */
10
+ // eslint-disable-next-line complexity -- Temporary
11
+ const hasRejectValue = (node, innerFunction, isAsync) => {
12
+ if (!node) {
13
+ return false;
14
+ }
15
+
16
+ switch (node.type) {
17
+ case 'ArrowFunctionExpression':
18
+ case 'FunctionDeclaration':
19
+ case 'FunctionExpression': {
20
+ // For inner functions in async contexts, check if they throw
21
+ // (they could be called and cause rejection)
22
+ if (innerFunction) {
23
+ // Check inner functions for throws - if called from async context, throws become rejections
24
+ const innerIsAsync = node.async;
25
+ // Pass isAsync=true if the inner function is async OR if we're already in an async context
26
+ return hasRejectValue(/** @type {import('eslint').Rule.Node} */ (node.body), false, innerIsAsync || isAsync);
27
+ }
28
+
29
+ // This is the top-level function we're checking
30
+ return hasRejectValue(/** @type {import('eslint').Rule.Node} */ (node.body), true, node.async);
31
+ }
32
+
33
+ case 'BlockStatement': {
34
+ return node.body.some((bodyNode) => {
35
+ return hasRejectValue(/** @type {import('eslint').Rule.Node} */ (bodyNode), innerFunction, isAsync);
36
+ });
37
+ }
38
+
39
+ case 'CallExpression': {
40
+ // Check for Promise.reject()
41
+ if (node.callee.type === 'MemberExpression' &&
42
+ node.callee.object.type === 'Identifier' &&
43
+ node.callee.object.name === 'Promise' &&
44
+ node.callee.property.type === 'Identifier' &&
45
+ node.callee.property.name === 'reject') {
46
+ return true;
47
+ }
48
+
49
+ // Check for reject() call (in Promise executor context)
50
+ if (node.callee.type === 'Identifier' && node.callee.name === 'reject') {
51
+ return true;
52
+ }
53
+
54
+ // Check if this is calling an inner function that might reject
55
+ if (innerFunction && node.callee.type === 'Identifier') {
56
+ // We found a function call inside - check if it could be calling a function that rejects
57
+ // We'll handle this in function body traversal
58
+ return false;
59
+ }
60
+
61
+ return false;
62
+ }
63
+
64
+ case 'DoWhileStatement':
65
+ case 'ForInStatement':
66
+ case 'ForOfStatement':
67
+ case 'ForStatement':
68
+ case 'LabeledStatement':
69
+ case 'WhileStatement':
70
+
71
+ case 'WithStatement': {
72
+ return hasRejectValue(/** @type {import('eslint').Rule.Node} */ (node.body), innerFunction, isAsync);
73
+ }
74
+
75
+ case 'ExpressionStatement': {
76
+ return hasRejectValue(/** @type {import('eslint').Rule.Node} */ (node.expression), innerFunction, isAsync);
77
+ }
78
+
79
+ case 'IfStatement': {
80
+ return hasRejectValue(/** @type {import('eslint').Rule.Node} */ (node.consequent), innerFunction, isAsync) || hasRejectValue(/** @type {import('eslint').Rule.Node} */ (node.alternate), innerFunction, isAsync);
81
+ }
82
+
83
+ case 'NewExpression': {
84
+ // Check for new Promise((resolve, reject) => { reject(...) })
85
+ if (node.callee.type === 'Identifier' && node.callee.name === 'Promise' && node.arguments.length > 0) {
86
+ const executor = node.arguments[0];
87
+ if (executor.type === 'ArrowFunctionExpression' || executor.type === 'FunctionExpression') {
88
+ // Check if the executor has reject() calls
89
+ return hasRejectValue(/** @type {import('eslint').Rule.Node} */ (executor.body), false, false);
90
+ }
91
+ }
92
+
93
+ return false;
94
+ }
95
+
96
+ case 'ReturnStatement': {
97
+ if (node.argument) {
98
+ return hasRejectValue(/** @type {import('eslint').Rule.Node} */ (node.argument), innerFunction, isAsync);
99
+ }
100
+
101
+ return false;
102
+ }
103
+
104
+ case 'SwitchStatement': {
105
+ return node.cases.some(
106
+ (someCase) => {
107
+ return someCase.consequent.some((nde) => {
108
+ return hasRejectValue(/** @type {import('eslint').Rule.Node} */ (nde), innerFunction, isAsync);
109
+ });
110
+ },
111
+ );
112
+ }
113
+
114
+ // Throw statements in async functions become rejections
115
+ case 'ThrowStatement': {
116
+ return isAsync === true;
117
+ }
118
+
119
+ case 'TryStatement': {
120
+ return hasRejectValue(/** @type {import('eslint').Rule.Node} */ (node.handler && node.handler.body), innerFunction, isAsync) ||
121
+ hasRejectValue(/** @type {import('eslint').Rule.Node} */ (node.finalizer), innerFunction, isAsync);
122
+ }
123
+
124
+ default: {
125
+ return false;
126
+ }
127
+ }
128
+ };
129
+
130
+ /**
131
+ * We can skip checking for a rejects value, in case the documentation is inherited
132
+ * or the method is abstract.
133
+ * @param {import('../iterateJsdoc.js').Utils} utils
134
+ * @returns {boolean}
135
+ */
136
+ const canSkip = (utils) => {
137
+ return utils.hasATag([
138
+ 'abstract',
139
+ 'virtual',
140
+ 'type',
141
+ ]) ||
142
+ utils.avoidDocs();
143
+ };
144
+
145
+ export default iterateJsdoc(({
146
+ node,
147
+ report,
148
+ utils,
149
+ }) => {
150
+ if (canSkip(utils)) {
151
+ return;
152
+ }
153
+
154
+ const tagName = /** @type {string} */ (utils.getPreferredTagName({
155
+ tagName: 'rejects',
156
+ }));
157
+ if (!tagName) {
158
+ return;
159
+ }
160
+
161
+ const tags = utils.getTags(tagName);
162
+ const iteratingFunction = utils.isIteratingFunction();
163
+
164
+ const [
165
+ tag,
166
+ ] = tags;
167
+ const missingRejectsTag = typeof tag === 'undefined' || tag === null;
168
+
169
+ const shouldReport = () => {
170
+ if (!missingRejectsTag) {
171
+ return false;
172
+ }
173
+
174
+ // Check if this is an async function or returns a Promise
175
+ const isAsync = utils.isAsync();
176
+ if (!isAsync && !iteratingFunction) {
177
+ return false;
178
+ }
179
+
180
+ // For async functions, check for throw statements
181
+ // For regular functions, check for Promise.reject or reject calls
182
+ return hasRejectValue(/** @type {import('eslint').Rule.Node} */ (node));
183
+ };
184
+
185
+ if (shouldReport()) {
186
+ report('Promise-rejecting function requires `@reject` tag');
187
+ }
188
+ }, {
189
+ contextDefaults: true,
190
+ meta: {
191
+ docs: {
192
+ description: 'Requires that Promise rejections are documented with `@rejects` tags.',
193
+ url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/require-rejects.md#repos-sticky-header',
194
+ },
195
+ schema: [
196
+ {
197
+ additionalProperties: false,
198
+ properties: {
199
+ contexts: {
200
+ description: `Set this to an array of strings representing the AST context
201
+ (or objects with optional \`context\` and \`comment\` properties) where you wish
202
+ the rule to be applied.
203
+
204
+ \`context\` defaults to \`any\` and \`comment\` defaults to no specific comment context.
205
+
206
+ Overrides the default contexts (\`ArrowFunctionExpression\`, \`FunctionDeclaration\`,
207
+ \`FunctionExpression\`).`,
208
+ items: {
209
+ anyOf: [
210
+ {
211
+ type: 'string',
212
+ },
213
+ {
214
+ additionalProperties: false,
215
+ properties: {
216
+ comment: {
217
+ type: 'string',
218
+ },
219
+ context: {
220
+ type: 'string',
221
+ },
222
+ },
223
+ type: 'object',
224
+ },
225
+ ],
226
+ },
227
+ type: 'array',
228
+ },
229
+ exemptedBy: {
230
+ description: `Array of tags (e.g., \`['type']\`) whose presence on the
231
+ document block avoids the need for a \`@rejects\`. Defaults to an array
232
+ with \`abstract\`, \`virtual\`, and \`type\`. If you set this array, it will overwrite the default,
233
+ so be sure to add back those tags if you wish their presence to cause
234
+ exemption of the rule.`,
235
+ items: {
236
+ type: 'string',
237
+ },
238
+ type: 'array',
239
+ },
240
+ },
241
+ type: 'object',
242
+ },
243
+ ],
244
+ type: 'suggestion',
245
+ },
246
+ });
package/src/rules.d.ts CHANGED
@@ -2196,6 +2196,39 @@ export interface Rules {
2196
2196
  /** Requires that each `@property` tag has a type value (in curly brackets). */
2197
2197
  "jsdoc/require-property-type": [];
2198
2198
 
2199
+ /** Requires that Promise rejections are documented with `@rejects` tags. */
2200
+ "jsdoc/require-rejects":
2201
+ | []
2202
+ | [
2203
+ {
2204
+ /**
2205
+ * Set this to an array of strings representing the AST context
2206
+ * (or objects with optional `context` and `comment` properties) where you wish
2207
+ * the rule to be applied.
2208
+ *
2209
+ * `context` defaults to `any` and `comment` defaults to no specific comment context.
2210
+ *
2211
+ * Overrides the default contexts (`ArrowFunctionExpression`, `FunctionDeclaration`,
2212
+ * `FunctionExpression`).
2213
+ */
2214
+ contexts?: (
2215
+ | string
2216
+ | {
2217
+ comment?: string;
2218
+ context?: string;
2219
+ }
2220
+ )[];
2221
+ /**
2222
+ * Array of tags (e.g., `['type']`) whose presence on the
2223
+ * document block avoids the need for a `@rejects`. Defaults to an array
2224
+ * with `abstract`, `virtual`, and `type`. If you set this array, it will overwrite the default,
2225
+ * so be sure to add back those tags if you wish their presence to cause
2226
+ * exemption of the rule.
2227
+ */
2228
+ exemptedBy?: string[];
2229
+ }
2230
+ ];
2231
+
2199
2232
  /** Requires that returns are documented with `@returns`. */
2200
2233
  "jsdoc/require-returns":
2201
2234
  | []