eslint-plugin-jsdoc 48.0.0 → 48.0.2

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.
Files changed (64) hide show
  1. package/package.json +2 -2
  2. package/src/WarnSettings.js +34 -0
  3. package/src/alignTransform.js +356 -0
  4. package/src/defaultTagOrder.js +168 -0
  5. package/src/exportParser.js +957 -0
  6. package/src/getDefaultTagStructureForMode.js +969 -0
  7. package/src/index.js +266 -0
  8. package/src/iterateJsdoc.js +2555 -0
  9. package/src/jsdocUtils.js +1693 -0
  10. package/src/rules/checkAccess.js +45 -0
  11. package/src/rules/checkAlignment.js +63 -0
  12. package/src/rules/checkExamples.js +594 -0
  13. package/src/rules/checkIndentation.js +75 -0
  14. package/src/rules/checkLineAlignment.js +364 -0
  15. package/src/rules/checkParamNames.js +404 -0
  16. package/src/rules/checkPropertyNames.js +152 -0
  17. package/src/rules/checkSyntax.js +30 -0
  18. package/src/rules/checkTagNames.js +314 -0
  19. package/src/rules/checkTypes.js +535 -0
  20. package/src/rules/checkValues.js +220 -0
  21. package/src/rules/emptyTags.js +88 -0
  22. package/src/rules/implementsOnClasses.js +64 -0
  23. package/src/rules/importsAsDependencies.js +131 -0
  24. package/src/rules/informativeDocs.js +182 -0
  25. package/src/rules/matchDescription.js +286 -0
  26. package/src/rules/matchName.js +147 -0
  27. package/src/rules/multilineBlocks.js +333 -0
  28. package/src/rules/noBadBlocks.js +109 -0
  29. package/src/rules/noBlankBlockDescriptions.js +69 -0
  30. package/src/rules/noBlankBlocks.js +53 -0
  31. package/src/rules/noDefaults.js +85 -0
  32. package/src/rules/noMissingSyntax.js +195 -0
  33. package/src/rules/noMultiAsterisks.js +134 -0
  34. package/src/rules/noRestrictedSyntax.js +91 -0
  35. package/src/rules/noTypes.js +73 -0
  36. package/src/rules/noUndefinedTypes.js +328 -0
  37. package/src/rules/requireAsteriskPrefix.js +189 -0
  38. package/src/rules/requireDescription.js +161 -0
  39. package/src/rules/requireDescriptionCompleteSentence.js +333 -0
  40. package/src/rules/requireExample.js +118 -0
  41. package/src/rules/requireFileOverview.js +154 -0
  42. package/src/rules/requireHyphenBeforeParamDescription.js +178 -0
  43. package/src/rules/requireJsdoc.js +629 -0
  44. package/src/rules/requireParam.js +592 -0
  45. package/src/rules/requireParamDescription.js +89 -0
  46. package/src/rules/requireParamName.js +55 -0
  47. package/src/rules/requireParamType.js +89 -0
  48. package/src/rules/requireProperty.js +48 -0
  49. package/src/rules/requirePropertyDescription.js +25 -0
  50. package/src/rules/requirePropertyName.js +25 -0
  51. package/src/rules/requirePropertyType.js +25 -0
  52. package/src/rules/requireReturns.js +238 -0
  53. package/src/rules/requireReturnsCheck.js +141 -0
  54. package/src/rules/requireReturnsDescription.js +59 -0
  55. package/src/rules/requireReturnsType.js +51 -0
  56. package/src/rules/requireThrows.js +111 -0
  57. package/src/rules/requireYields.js +216 -0
  58. package/src/rules/requireYieldsCheck.js +208 -0
  59. package/src/rules/sortTags.js +557 -0
  60. package/src/rules/tagLines.js +359 -0
  61. package/src/rules/textEscaping.js +146 -0
  62. package/src/rules/validTypes.js +368 -0
  63. package/src/tagNames.js +234 -0
  64. package/src/utils/hasReturnValue.js +549 -0
@@ -0,0 +1,333 @@
1
+ import iterateJsdoc from '../iterateJsdoc.js';
2
+ import escapeStringRegexp from 'escape-string-regexp';
3
+
4
+ const otherDescriptiveTags = new Set([
5
+ // 'copyright' and 'see' might be good addition, but as the former may be
6
+ // sensitive text, and the latter may have just a link, they are not
7
+ // included by default
8
+ 'summary', 'file', 'fileoverview', 'overview', 'classdesc', 'todo',
9
+ 'deprecated', 'throws', 'exception', 'yields', 'yield',
10
+ ]);
11
+
12
+ /**
13
+ * @param {string} text
14
+ * @returns {string[]}
15
+ */
16
+ const extractParagraphs = (text) => {
17
+ return text.split(/(?<![;:])\n\n/u);
18
+ };
19
+
20
+ /**
21
+ * @param {string} text
22
+ * @param {string|RegExp} abbreviationsRegex
23
+ * @returns {string[]}
24
+ */
25
+ const extractSentences = (text, abbreviationsRegex) => {
26
+ const txt = text
27
+ // Remove all {} tags.
28
+ .replaceAll(/(?<!^)\{[\s\S]*?\}\s*/gu, '')
29
+
30
+ // Remove custom abbreviations
31
+ .replace(abbreviationsRegex, '');
32
+
33
+ const sentenceEndGrouping = /([.?!])(?:\s+|$)/ug;
34
+
35
+ const puncts = [
36
+ ...txt.matchAll(sentenceEndGrouping),
37
+ ].map((sentEnd) => {
38
+ return sentEnd[0];
39
+ });
40
+
41
+ return txt
42
+ .split(/[.?!](?:\s+|$)/u)
43
+
44
+ // Re-add the dot.
45
+ .map((sentence, idx) => {
46
+ return !puncts[idx] && /^\s*$/u.test(sentence) ? sentence : `${sentence}${puncts[idx] || ''}`;
47
+ });
48
+ };
49
+
50
+ /**
51
+ * @param {string} text
52
+ * @returns {boolean}
53
+ */
54
+ const isNewLinePrecededByAPeriod = (text) => {
55
+ /** @type {boolean} */
56
+ let lastLineEndsSentence;
57
+
58
+ const lines = text.split('\n');
59
+
60
+ return !lines.some((line) => {
61
+ if (lastLineEndsSentence === false && /^[A-Z][a-z]/u.test(line)) {
62
+ return true;
63
+ }
64
+
65
+ lastLineEndsSentence = /[.:?!|]$/u.test(line);
66
+
67
+ return false;
68
+ });
69
+ };
70
+
71
+ /**
72
+ * @param {string} str
73
+ * @returns {boolean}
74
+ */
75
+ const isCapitalized = (str) => {
76
+ return str[0] === str[0].toUpperCase();
77
+ };
78
+
79
+ /**
80
+ * @param {string} str
81
+ * @returns {boolean}
82
+ */
83
+ const isTable = (str) => {
84
+ return str.charAt(0) === '|';
85
+ };
86
+
87
+ /**
88
+ * @param {string} str
89
+ * @returns {string}
90
+ */
91
+ const capitalize = (str) => {
92
+ return str.charAt(0).toUpperCase() + str.slice(1);
93
+ };
94
+
95
+ /**
96
+ * @param {string} description
97
+ * @param {import('../iterateJsdoc.js').Report} reportOrig
98
+ * @param {import('eslint').Rule.Node} jsdocNode
99
+ * @param {string|RegExp} abbreviationsRegex
100
+ * @param {import('eslint').SourceCode} sourceCode
101
+ * @param {import('comment-parser').Spec|{
102
+ * line: import('../iterateJsdoc.js').Integer
103
+ * }} tag
104
+ * @param {boolean} newlineBeforeCapsAssumesBadSentenceEnd
105
+ * @returns {boolean}
106
+ */
107
+ const validateDescription = (
108
+ description, reportOrig, jsdocNode, abbreviationsRegex,
109
+ sourceCode, tag, newlineBeforeCapsAssumesBadSentenceEnd,
110
+ ) => {
111
+ if (!description || (/^\n+$/u).test(description)) {
112
+ return false;
113
+ }
114
+
115
+ const paragraphs = extractParagraphs(description).filter(Boolean);
116
+
117
+ return paragraphs.some((paragraph, parIdx) => {
118
+ const sentences = extractSentences(paragraph, abbreviationsRegex);
119
+
120
+ const fix = /** @type {import('eslint').Rule.ReportFixer} */ (fixer) => {
121
+ let text = sourceCode.getText(jsdocNode);
122
+
123
+ if (!/[.:?!]$/u.test(paragraph)) {
124
+ const line = paragraph.split('\n').filter(Boolean).pop();
125
+ text = text.replace(new RegExp(`${escapeStringRegexp(
126
+ /** @type {string} */
127
+ (line),
128
+ )}$`, 'mu'), `${line}.`);
129
+ }
130
+
131
+ for (const sentence of sentences.filter((sentence_) => {
132
+ return !(/^\s*$/u).test(sentence_) && !isCapitalized(sentence_) &&
133
+ !isTable(sentence_);
134
+ })) {
135
+ const beginning = sentence.split('\n')[0];
136
+
137
+ if ('tag' in tag && tag.tag) {
138
+ const reg = new RegExp(`(@${escapeStringRegexp(tag.tag)}.*)${escapeStringRegexp(beginning)}`, 'u');
139
+
140
+ text = text.replace(reg, (_$0, $1) => {
141
+ return $1 + capitalize(beginning);
142
+ });
143
+ } else {
144
+ text = text.replace(new RegExp('((?:[.?!]|\\*|\\})\\s*)' + escapeStringRegexp(beginning), 'u'), '$1' + capitalize(beginning));
145
+ }
146
+ }
147
+
148
+ return fixer.replaceText(jsdocNode, text);
149
+ };
150
+
151
+ /**
152
+ * @param {string} msg
153
+ * @param {import('eslint').Rule.ReportFixer | null | undefined} fixer
154
+ * @param {{
155
+ * line?: number | undefined;
156
+ * column?: number | undefined;
157
+ * } | (import('comment-parser').Spec & {
158
+ * line?: number | undefined;
159
+ * column?: number | undefined;
160
+ * })} tagObj
161
+ * @returns {void}
162
+ */
163
+ const report = (msg, fixer, tagObj) => {
164
+ if ('line' in tagObj) {
165
+ /**
166
+ * @type {{
167
+ * line: number;
168
+ * }}
169
+ */ (tagObj).line += parIdx * 2;
170
+ } else {
171
+ /** @type {import('comment-parser').Spec} */ (
172
+ tagObj
173
+ ).source[0].number += parIdx * 2;
174
+ }
175
+
176
+ // Avoid errors if old column doesn't exist here
177
+ tagObj.column = 0;
178
+ reportOrig(msg, fixer, tagObj);
179
+ };
180
+
181
+ if (sentences.some((sentence) => {
182
+ return (/^[.?!]$/u).test(sentence);
183
+ })) {
184
+ report('Sentences must be more than punctuation.', null, tag);
185
+ }
186
+
187
+ if (sentences.some((sentence) => {
188
+ return !(/^\s*$/u).test(sentence) && !isCapitalized(sentence) && !isTable(sentence);
189
+ })) {
190
+ report('Sentences should start with an uppercase character.', fix, tag);
191
+ }
192
+
193
+ const paragraphNoAbbreviations = paragraph.replace(abbreviationsRegex, '');
194
+
195
+ if (!/(?:[.?!|]|```)\s*$/u.test(paragraphNoAbbreviations)) {
196
+ report('Sentences must end with a period.', fix, tag);
197
+ return true;
198
+ }
199
+
200
+ if (newlineBeforeCapsAssumesBadSentenceEnd && !isNewLinePrecededByAPeriod(paragraphNoAbbreviations)) {
201
+ report('A line of text is started with an uppercase character, but the preceding line does not end the sentence.', null, tag);
202
+
203
+ return true;
204
+ }
205
+
206
+ return false;
207
+ });
208
+ };
209
+
210
+ export default iterateJsdoc(({
211
+ sourceCode,
212
+ context,
213
+ jsdoc,
214
+ report,
215
+ jsdocNode,
216
+ utils,
217
+ }) => {
218
+ const /** @type {{abbreviations: string[], newlineBeforeCapsAssumesBadSentenceEnd: boolean}} */ {
219
+ abbreviations = [],
220
+ newlineBeforeCapsAssumesBadSentenceEnd = false,
221
+ } = context.options[0] || {};
222
+
223
+ const abbreviationsRegex = abbreviations.length ?
224
+ new RegExp('\\b' + abbreviations.map((abbreviation) => {
225
+ return escapeStringRegexp(abbreviation.replaceAll(/\.$/ug, '') + '.');
226
+ }).join('|') + '(?:$|\\s)', 'gu') :
227
+ '';
228
+
229
+ let {
230
+ description,
231
+ } = utils.getDescription();
232
+
233
+ const indices = [
234
+ ...description.matchAll(/```[\s\S]*```/gu),
235
+ ].map((match) => {
236
+ const {
237
+ index,
238
+ } = match;
239
+ const [
240
+ {
241
+ length,
242
+ },
243
+ ] = match;
244
+ return {
245
+ index,
246
+ length,
247
+ };
248
+ }).reverse();
249
+
250
+ for (const {
251
+ index,
252
+ length,
253
+ } of indices) {
254
+ description = description.slice(0, index) +
255
+ description.slice(/** @type {import('../iterateJsdoc.js').Integer} */ (
256
+ index
257
+ ) + length);
258
+ }
259
+
260
+ if (validateDescription(description, report, jsdocNode, abbreviationsRegex, sourceCode, {
261
+ line: jsdoc.source[0].number + 1,
262
+ }, newlineBeforeCapsAssumesBadSentenceEnd)) {
263
+ return;
264
+ }
265
+
266
+ utils.forEachPreferredTag('description', (matchingJsdocTag) => {
267
+ const desc = `${matchingJsdocTag.name} ${utils.getTagDescription(matchingJsdocTag)}`.trim();
268
+ validateDescription(desc, report, jsdocNode, abbreviationsRegex, sourceCode, matchingJsdocTag, newlineBeforeCapsAssumesBadSentenceEnd);
269
+ }, true);
270
+
271
+ const {
272
+ tagsWithNames,
273
+ } = utils.getTagsByType(jsdoc.tags);
274
+ const tagsWithoutNames = utils.filterTags(({
275
+ tag: tagName,
276
+ }) => {
277
+ return otherDescriptiveTags.has(tagName) ||
278
+ utils.hasOptionTag(tagName) && !tagsWithNames.some(({
279
+ tag,
280
+ }) => {
281
+ // If user accidentally adds tags with names (or like `returns`
282
+ // get parsed as having names), do not add to this list
283
+ return tag === tagName;
284
+ });
285
+ });
286
+
287
+ tagsWithNames.some((tag) => {
288
+ const desc = /** @type {string} */ (
289
+ utils.getTagDescription(tag)
290
+ ).replace(/^- /u, '').trimEnd();
291
+
292
+ return validateDescription(desc, report, jsdocNode, abbreviationsRegex, sourceCode, tag, newlineBeforeCapsAssumesBadSentenceEnd);
293
+ });
294
+
295
+ tagsWithoutNames.some((tag) => {
296
+ const desc = `${tag.name} ${utils.getTagDescription(tag)}`.trim();
297
+
298
+ return validateDescription(desc, report, jsdocNode, abbreviationsRegex, sourceCode, tag, newlineBeforeCapsAssumesBadSentenceEnd);
299
+ });
300
+ }, {
301
+ iterateAllJsdocs: true,
302
+ meta: {
303
+ docs: {
304
+ description: 'Requires that block description, explicit `@description`, and `@param`/`@returns` tag descriptions are written in complete sentences.',
305
+ url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/require-description-complete-sentence.md#repos-sticky-header',
306
+ },
307
+ fixable: 'code',
308
+ schema: [
309
+ {
310
+ additionalProperties: false,
311
+ properties: {
312
+ abbreviations: {
313
+ items: {
314
+ type: 'string',
315
+ },
316
+ type: 'array',
317
+ },
318
+ newlineBeforeCapsAssumesBadSentenceEnd: {
319
+ type: 'boolean',
320
+ },
321
+ tags: {
322
+ items: {
323
+ type: 'string',
324
+ },
325
+ type: 'array',
326
+ },
327
+ },
328
+ type: 'object',
329
+ },
330
+ ],
331
+ type: 'suggestion',
332
+ },
333
+ });
@@ -0,0 +1,118 @@
1
+ import iterateJsdoc from '../iterateJsdoc.js';
2
+
3
+ export default iterateJsdoc(({
4
+ context,
5
+ jsdoc,
6
+ report,
7
+ utils,
8
+ }) => {
9
+ if (utils.avoidDocs()) {
10
+ return;
11
+ }
12
+
13
+ const {
14
+ enableFixer = true,
15
+ exemptNoArguments = false,
16
+ } = context.options[0] || {};
17
+
18
+ const targetTagName = 'example';
19
+
20
+ const functionExamples = jsdoc.tags.filter(({
21
+ tag,
22
+ }) => {
23
+ return tag === targetTagName;
24
+ });
25
+
26
+ if (!functionExamples.length) {
27
+ if (exemptNoArguments && utils.isIteratingFunction() &&
28
+ !utils.hasParams()
29
+ ) {
30
+ return;
31
+ }
32
+
33
+ utils.reportJSDoc(`Missing JSDoc @${targetTagName} declaration.`, null, () => {
34
+ if (enableFixer) {
35
+ utils.addTag(targetTagName);
36
+ }
37
+ });
38
+
39
+ return;
40
+ }
41
+
42
+ for (const example of functionExamples) {
43
+ const exampleContent = `${example.name} ${utils.getTagDescription(example)}`
44
+ .trim()
45
+ .split('\n')
46
+ .filter(Boolean);
47
+
48
+ if (!exampleContent.length) {
49
+ report(`Missing JSDoc @${targetTagName} description.`, null, example);
50
+ }
51
+ }
52
+ }, {
53
+ contextDefaults: true,
54
+ meta: {
55
+ docs: {
56
+ description: 'Requires that all functions have examples.',
57
+ url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/require-example.md#repos-sticky-header',
58
+ },
59
+ fixable: 'code',
60
+ schema: [
61
+ {
62
+ additionalProperties: false,
63
+ properties: {
64
+ checkConstructors: {
65
+ default: true,
66
+ type: 'boolean',
67
+ },
68
+ checkGetters: {
69
+ default: false,
70
+ type: 'boolean',
71
+ },
72
+ checkSetters: {
73
+ default: false,
74
+ type: 'boolean',
75
+ },
76
+ contexts: {
77
+ items: {
78
+ anyOf: [
79
+ {
80
+ type: 'string',
81
+ },
82
+ {
83
+ additionalProperties: false,
84
+ properties: {
85
+ comment: {
86
+ type: 'string',
87
+ },
88
+ context: {
89
+ type: 'string',
90
+ },
91
+ },
92
+ type: 'object',
93
+ },
94
+ ],
95
+ },
96
+ type: 'array',
97
+ },
98
+ enableFixer: {
99
+ default: true,
100
+ type: 'boolean',
101
+ },
102
+ exemptedBy: {
103
+ items: {
104
+ type: 'string',
105
+ },
106
+ type: 'array',
107
+ },
108
+ exemptNoArguments: {
109
+ default: false,
110
+ type: 'boolean',
111
+ },
112
+ },
113
+ type: 'object',
114
+ },
115
+ ],
116
+ type: 'suggestion',
117
+ },
118
+ });
@@ -0,0 +1,154 @@
1
+ import iterateJsdoc from '../iterateJsdoc.js';
2
+
3
+ const defaultTags = {
4
+ file: {
5
+ initialCommentsOnly: true,
6
+ mustExist: true,
7
+ preventDuplicates: true,
8
+ },
9
+ };
10
+
11
+ /**
12
+ * @param {import('../iterateJsdoc.js').StateObject} state
13
+ * @returns {void}
14
+ */
15
+ const setDefaults = (state) => {
16
+ // First iteration
17
+ if (!state.globalTags) {
18
+ state.globalTags = {};
19
+ state.hasDuplicates = {};
20
+ state.hasTag = {};
21
+ state.hasNonCommentBeforeTag = {};
22
+ }
23
+ };
24
+
25
+ export default iterateJsdoc(({
26
+ jsdocNode,
27
+ state,
28
+ utils,
29
+ context,
30
+ }) => {
31
+ const {
32
+ tags = defaultTags,
33
+ } = context.options[0] || {};
34
+
35
+ setDefaults(state);
36
+
37
+ for (const tagName of Object.keys(tags)) {
38
+ const targetTagName = /** @type {string} */ (utils.getPreferredTagName({
39
+ tagName,
40
+ }));
41
+
42
+ const hasTag = Boolean(targetTagName && utils.hasTag(targetTagName));
43
+
44
+ state.hasTag[tagName] = hasTag || state.hasTag[tagName];
45
+
46
+ const hasDuplicate = state.hasDuplicates[tagName];
47
+
48
+ if (hasDuplicate === false) {
49
+ // Was marked before, so if a tag now, is a dupe
50
+ state.hasDuplicates[tagName] = hasTag;
51
+ } else if (!hasDuplicate && hasTag) {
52
+ // No dupes set before, but has first tag, so change state
53
+ // from `undefined` to `false` so can detect next time
54
+ state.hasDuplicates[tagName] = false;
55
+ state.hasNonCommentBeforeTag[tagName] = state.hasNonComment &&
56
+ state.hasNonComment < jsdocNode.range[0];
57
+ }
58
+ }
59
+ }, {
60
+ exit ({
61
+ context,
62
+ state,
63
+ utils,
64
+ }) {
65
+ setDefaults(state);
66
+ const {
67
+ tags = defaultTags,
68
+ } = context.options[0] || {};
69
+
70
+ for (const [
71
+ tagName,
72
+ {
73
+ mustExist = false,
74
+ preventDuplicates = false,
75
+ initialCommentsOnly = false,
76
+ },
77
+ ] of Object.entries(tags)) {
78
+ const obj = utils.getPreferredTagNameObject({
79
+ tagName,
80
+ });
81
+ if (obj && typeof obj === 'object' && 'blocked' in obj) {
82
+ utils.reportSettings(
83
+ `\`settings.jsdoc.tagNamePreference\` cannot block @${obj.tagName} ` +
84
+ 'for the `require-file-overview` rule',
85
+ );
86
+ } else {
87
+ const targetTagName = (
88
+ obj && typeof obj === 'object' && obj.replacement
89
+ ) || obj;
90
+ if (mustExist && !state.hasTag[tagName]) {
91
+ utils.reportSettings(`Missing @${targetTagName}`);
92
+ }
93
+
94
+ if (preventDuplicates && state.hasDuplicates[tagName]) {
95
+ utils.reportSettings(
96
+ `Duplicate @${targetTagName}`,
97
+ );
98
+ }
99
+
100
+ if (initialCommentsOnly &&
101
+ state.hasNonCommentBeforeTag[tagName]
102
+ ) {
103
+ utils.reportSettings(
104
+ `@${targetTagName} should be at the beginning of the file`,
105
+ );
106
+ }
107
+ }
108
+ }
109
+ },
110
+ iterateAllJsdocs: true,
111
+ meta: {
112
+ docs: {
113
+ description: 'Checks that all files have one `@file`, `@fileoverview`, or `@overview` tag at the beginning of the file.',
114
+ url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/require-file-overview.md#repos-sticky-header',
115
+ },
116
+ schema: [
117
+ {
118
+ additionalProperties: false,
119
+ properties: {
120
+ tags: {
121
+ patternProperties: {
122
+ '.*': {
123
+ additionalProperties: false,
124
+ properties: {
125
+ initialCommentsOnly: {
126
+ type: 'boolean',
127
+ },
128
+ mustExist: {
129
+ type: 'boolean',
130
+ },
131
+ preventDuplicates: {
132
+ type: 'boolean',
133
+ },
134
+ },
135
+ type: 'object',
136
+ },
137
+ },
138
+ type: 'object',
139
+ },
140
+ },
141
+ type: 'object',
142
+ },
143
+ ],
144
+ type: 'suggestion',
145
+ },
146
+ nonComment ({
147
+ state,
148
+ node,
149
+ }) {
150
+ if (!state.hasNonComment) {
151
+ state.hasNonComment = node.range[0];
152
+ }
153
+ },
154
+ });