eslint-plugin-jsdoc 48.8.3 → 48.9.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.
- package/README.md +7 -0
- package/dist/WarnSettings.cjs +2 -2
- package/dist/WarnSettings.cjs.map +1 -1
- package/dist/getJsdocProcessorPlugin.cjs +538 -0
- package/dist/getJsdocProcessorPlugin.cjs.map +1 -0
- package/dist/index.cjs +78 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/iterateJsdoc.cjs +58 -112
- package/dist/iterateJsdoc.cjs.map +1 -1
- package/dist/jsdocUtils.cjs +226 -48
- package/dist/jsdocUtils.cjs.map +1 -1
- package/dist/rules/convertToJsdocComments.cjs +5 -6
- package/dist/rules/convertToJsdocComments.cjs.map +1 -1
- package/dist/rules/requireJsdoc.cjs +7 -7
- package/dist/rules/requireJsdoc.cjs.map +1 -1
- package/eslint.config.js +3 -2
- package/package.json +12 -4
- package/src/WarnSettings.js +2 -2
- package/src/getJsdocProcessorPlugin.js +602 -0
- package/src/index.js +106 -0
- package/src/iterateJsdoc.js +21 -97
- package/src/jsdocUtils.js +191 -7
- package/src/rules/convertToJsdocComments.js +10 -6
- package/src/rules/requireJsdoc.js +16 -8
|
@@ -0,0 +1,602 @@
|
|
|
1
|
+
// Todo: Support TS by fenced block type
|
|
2
|
+
|
|
3
|
+
import {readFileSync} from 'fs';
|
|
4
|
+
import * as espree from 'espree';
|
|
5
|
+
import {
|
|
6
|
+
getRegexFromString,
|
|
7
|
+
forEachPreferredTag,
|
|
8
|
+
getTagDescription,
|
|
9
|
+
getPreferredTagName,
|
|
10
|
+
hasTag,
|
|
11
|
+
} from './jsdocUtils.js';
|
|
12
|
+
import {
|
|
13
|
+
parseComment,
|
|
14
|
+
} from '@es-joy/jsdoccomment';
|
|
15
|
+
|
|
16
|
+
const {version} = JSON.parse(
|
|
17
|
+
// @ts-expect-error `Buffer` is ok for `JSON.parse`
|
|
18
|
+
readFileSync('./package.json')
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
// const zeroBasedLineIndexAdjust = -1;
|
|
22
|
+
const likelyNestedJSDocIndentSpace = 1;
|
|
23
|
+
const preTagSpaceLength = 1;
|
|
24
|
+
|
|
25
|
+
// If a space is present, we should ignore it
|
|
26
|
+
const firstLinePrefixLength = preTagSpaceLength;
|
|
27
|
+
|
|
28
|
+
const hasCaptionRegex = /^\s*<caption>([\s\S]*?)<\/caption>/u;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @param {string} str
|
|
32
|
+
* @returns {string}
|
|
33
|
+
*/
|
|
34
|
+
const escapeStringRegexp = (str) => {
|
|
35
|
+
return str.replaceAll(/[.*+?^${}()|[\]\\]/gu, '\\$&');
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @param {string} str
|
|
40
|
+
* @param {string} ch
|
|
41
|
+
* @returns {import('./iterateJsdoc.js').Integer}
|
|
42
|
+
*/
|
|
43
|
+
const countChars = (str, ch) => {
|
|
44
|
+
return (str.match(new RegExp(escapeStringRegexp(ch), 'gu')) || []).length;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {string} text
|
|
49
|
+
* @returns {[
|
|
50
|
+
* import('./iterateJsdoc.js').Integer,
|
|
51
|
+
* import('./iterateJsdoc.js').Integer
|
|
52
|
+
* ]}
|
|
53
|
+
*/
|
|
54
|
+
const getLinesCols = (text) => {
|
|
55
|
+
const matchLines = countChars(text, '\n');
|
|
56
|
+
|
|
57
|
+
const colDelta = matchLines ?
|
|
58
|
+
text.slice(text.lastIndexOf('\n') + 1).length :
|
|
59
|
+
text.length;
|
|
60
|
+
|
|
61
|
+
return [
|
|
62
|
+
matchLines, colDelta,
|
|
63
|
+
];
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @typedef {number} Integer
|
|
68
|
+
*/
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @typedef {object} JsdocProcessorOptions
|
|
72
|
+
* @property {boolean} [captionRequired]
|
|
73
|
+
* @property {Integer} [paddedIndent]
|
|
74
|
+
* @property {boolean} [checkDefaults]
|
|
75
|
+
* @property {boolean} [checkParams]
|
|
76
|
+
* @property {boolean} [checkExamples]
|
|
77
|
+
* @property {boolean} [checkProperties]
|
|
78
|
+
* @property {string} [matchingFileName]
|
|
79
|
+
* @property {string} [matchingFileNameDefaults]
|
|
80
|
+
* @property {string} [matchingFileNameParams]
|
|
81
|
+
* @property {string} [matchingFileNameProperties]
|
|
82
|
+
* @property {string} [exampleCodeRegex]
|
|
83
|
+
* @property {string} [rejectExampleCodeRegex]
|
|
84
|
+
* @property {"script"|"module"} [sourceType]
|
|
85
|
+
* @property {import('eslint').Linter.FlatConfigParserModule} [parser]
|
|
86
|
+
*/
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* We use a function for the ability of the user to pass in a config, but
|
|
90
|
+
* without requiring all users of the plugin to do so.
|
|
91
|
+
* @param {JsdocProcessorOptions} [options]
|
|
92
|
+
*/
|
|
93
|
+
export const getJsdocProcessorPlugin = (options = {}) => {
|
|
94
|
+
const {
|
|
95
|
+
exampleCodeRegex = null,
|
|
96
|
+
rejectExampleCodeRegex = null,
|
|
97
|
+
checkExamples = true,
|
|
98
|
+
checkDefaults = false,
|
|
99
|
+
checkParams = false,
|
|
100
|
+
checkProperties = false,
|
|
101
|
+
matchingFileName = null,
|
|
102
|
+
matchingFileNameDefaults = null,
|
|
103
|
+
matchingFileNameParams = null,
|
|
104
|
+
matchingFileNameProperties = null,
|
|
105
|
+
paddedIndent = 0,
|
|
106
|
+
captionRequired = false,
|
|
107
|
+
sourceType = 'module',
|
|
108
|
+
parser = undefined
|
|
109
|
+
} = options;
|
|
110
|
+
|
|
111
|
+
/** @type {RegExp} */
|
|
112
|
+
let exampleCodeRegExp;
|
|
113
|
+
/** @type {RegExp} */
|
|
114
|
+
let rejectExampleCodeRegExp;
|
|
115
|
+
|
|
116
|
+
if (exampleCodeRegex) {
|
|
117
|
+
exampleCodeRegExp = getRegexFromString(exampleCodeRegex);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (rejectExampleCodeRegex) {
|
|
121
|
+
rejectExampleCodeRegExp = getRegexFromString(rejectExampleCodeRegex);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* @type {{
|
|
126
|
+
* targetTagName: string,
|
|
127
|
+
* ext: string,
|
|
128
|
+
* codeStartLine: number,
|
|
129
|
+
* codeStartCol: number,
|
|
130
|
+
* nonJSPrefacingCols: number,
|
|
131
|
+
* commentLineCols: [number, number]
|
|
132
|
+
* }[]}
|
|
133
|
+
*/
|
|
134
|
+
const otherInfo = [];
|
|
135
|
+
|
|
136
|
+
/** @type {import('eslint').Linter.LintMessage[]} */
|
|
137
|
+
let extraMessages = [];
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* @param {import('./iterateJsdoc.js').JsdocBlockWithInline} jsdoc
|
|
141
|
+
* @param {string} jsFileName
|
|
142
|
+
* @param {[number, number]} commentLineCols
|
|
143
|
+
*/
|
|
144
|
+
const getTextsAndFileNames = (jsdoc, jsFileName, commentLineCols) => {
|
|
145
|
+
/**
|
|
146
|
+
* @type {{
|
|
147
|
+
* text: string,
|
|
148
|
+
* filename: string|null|undefined
|
|
149
|
+
* }[]}
|
|
150
|
+
*/
|
|
151
|
+
const textsAndFileNames = [];
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* @param {{
|
|
155
|
+
* filename: string|null,
|
|
156
|
+
* defaultFileName: string|undefined,
|
|
157
|
+
* source: string,
|
|
158
|
+
* targetTagName: string,
|
|
159
|
+
* rules?: import('eslint').Linter.RulesRecord|undefined,
|
|
160
|
+
* lines?: import('./iterateJsdoc.js').Integer,
|
|
161
|
+
* cols?: import('./iterateJsdoc.js').Integer,
|
|
162
|
+
* skipInit?: boolean,
|
|
163
|
+
* ext: string,
|
|
164
|
+
* sources?: {
|
|
165
|
+
* nonJSPrefacingCols: import('./iterateJsdoc.js').Integer,
|
|
166
|
+
* nonJSPrefacingLines: import('./iterateJsdoc.js').Integer,
|
|
167
|
+
* string: string,
|
|
168
|
+
* }[],
|
|
169
|
+
* tag?: import('comment-parser').Spec & {
|
|
170
|
+
* line?: import('./iterateJsdoc.js').Integer,
|
|
171
|
+
* }|{
|
|
172
|
+
* line: import('./iterateJsdoc.js').Integer,
|
|
173
|
+
* }
|
|
174
|
+
* }} cfg
|
|
175
|
+
*/
|
|
176
|
+
const checkSource = ({
|
|
177
|
+
filename,
|
|
178
|
+
ext,
|
|
179
|
+
defaultFileName,
|
|
180
|
+
lines = 0,
|
|
181
|
+
cols = 0,
|
|
182
|
+
skipInit,
|
|
183
|
+
source,
|
|
184
|
+
targetTagName,
|
|
185
|
+
sources = [],
|
|
186
|
+
tag = {
|
|
187
|
+
line: 0,
|
|
188
|
+
},
|
|
189
|
+
}) => {
|
|
190
|
+
if (!skipInit) {
|
|
191
|
+
sources.push({
|
|
192
|
+
nonJSPrefacingCols: cols,
|
|
193
|
+
nonJSPrefacingLines: lines,
|
|
194
|
+
string: source,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* @param {{
|
|
200
|
+
* nonJSPrefacingCols: import('./iterateJsdoc.js').Integer,
|
|
201
|
+
* nonJSPrefacingLines: import('./iterateJsdoc.js').Integer,
|
|
202
|
+
* string: string
|
|
203
|
+
* }} cfg
|
|
204
|
+
*/
|
|
205
|
+
const addSourceInfo = function ({
|
|
206
|
+
nonJSPrefacingCols,
|
|
207
|
+
nonJSPrefacingLines,
|
|
208
|
+
string,
|
|
209
|
+
}) {
|
|
210
|
+
const src = paddedIndent ?
|
|
211
|
+
string.replaceAll(new RegExp(`(^|\n) {${paddedIndent}}(?!$)`, 'gu'), '\n') :
|
|
212
|
+
string;
|
|
213
|
+
|
|
214
|
+
// Programmatic ESLint API: https://eslint.org/docs/developer-guide/nodejs-api
|
|
215
|
+
const file = filename || defaultFileName;
|
|
216
|
+
|
|
217
|
+
if (!('line' in tag)) {
|
|
218
|
+
tag.line = tag.source[0].number;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// NOTE: `tag.line` can be 0 if of form `/** @tag ... */`
|
|
222
|
+
const codeStartLine = /**
|
|
223
|
+
* @type {import('comment-parser').Spec & {
|
|
224
|
+
* line: import('./iterateJsdoc.js').Integer,
|
|
225
|
+
* }}
|
|
226
|
+
*/ (tag).line + nonJSPrefacingLines;
|
|
227
|
+
const codeStartCol = likelyNestedJSDocIndentSpace;
|
|
228
|
+
|
|
229
|
+
textsAndFileNames.push({
|
|
230
|
+
text: src,
|
|
231
|
+
filename: file,
|
|
232
|
+
});
|
|
233
|
+
otherInfo.push({
|
|
234
|
+
targetTagName,
|
|
235
|
+
ext,
|
|
236
|
+
codeStartLine,
|
|
237
|
+
codeStartCol,
|
|
238
|
+
nonJSPrefacingCols,
|
|
239
|
+
commentLineCols
|
|
240
|
+
});
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
for (const targetSource of sources) {
|
|
244
|
+
addSourceInfo(targetSource);
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
*
|
|
250
|
+
* @param {string|null} filename
|
|
251
|
+
* @param {string} [ext] Since `eslint-plugin-markdown` v2, and
|
|
252
|
+
* ESLint 7, this is the default which other JS-fenced rules will used.
|
|
253
|
+
* Formerly "md" was the default.
|
|
254
|
+
* @returns {{
|
|
255
|
+
* defaultFileName: string|undefined,
|
|
256
|
+
* filename: string|null,
|
|
257
|
+
* ext: string
|
|
258
|
+
* }}
|
|
259
|
+
*/
|
|
260
|
+
const getFilenameInfo = (filename, ext = 'md/*.js') => {
|
|
261
|
+
let defaultFileName;
|
|
262
|
+
if (!filename) {
|
|
263
|
+
if (typeof jsFileName === 'string' && jsFileName.includes('.')) {
|
|
264
|
+
defaultFileName = jsFileName.replace(/\.[^.]*$/u, `.${ext}`);
|
|
265
|
+
} else {
|
|
266
|
+
defaultFileName = `dummy.${ext}`;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
ext,
|
|
272
|
+
defaultFileName,
|
|
273
|
+
filename,
|
|
274
|
+
};
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
if (checkDefaults) {
|
|
278
|
+
const filenameInfo = getFilenameInfo(matchingFileNameDefaults, 'jsdoc-defaults');
|
|
279
|
+
forEachPreferredTag(jsdoc, 'default', (tag, targetTagName) => {
|
|
280
|
+
if (!tag.description.trim()) {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
checkSource({
|
|
285
|
+
source: `(${getTagDescription(tag)})`,
|
|
286
|
+
targetTagName,
|
|
287
|
+
...filenameInfo,
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (checkParams) {
|
|
293
|
+
const filenameInfo = getFilenameInfo(matchingFileNameParams, 'jsdoc-params');
|
|
294
|
+
forEachPreferredTag(jsdoc, 'param', (tag, targetTagName) => {
|
|
295
|
+
if (!tag.default || !tag.default.trim()) {
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
checkSource({
|
|
300
|
+
source: `(${tag.default})`,
|
|
301
|
+
targetTagName,
|
|
302
|
+
...filenameInfo,
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (checkProperties) {
|
|
308
|
+
const filenameInfo = getFilenameInfo(matchingFileNameProperties, 'jsdoc-properties');
|
|
309
|
+
forEachPreferredTag(jsdoc, 'property', (tag, targetTagName) => {
|
|
310
|
+
if (!tag.default || !tag.default.trim()) {
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
checkSource({
|
|
315
|
+
source: `(${tag.default})`,
|
|
316
|
+
targetTagName,
|
|
317
|
+
...filenameInfo,
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (!checkExamples) {
|
|
323
|
+
return textsAndFileNames;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const tagName = /** @type {string} */ (getPreferredTagName(jsdoc, {
|
|
327
|
+
tagName: 'example',
|
|
328
|
+
}));
|
|
329
|
+
if (!hasTag(jsdoc, tagName)) {
|
|
330
|
+
return textsAndFileNames;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const matchingFilenameInfo = getFilenameInfo(matchingFileName);
|
|
334
|
+
|
|
335
|
+
forEachPreferredTag(jsdoc, 'example', (tag, targetTagName) => {
|
|
336
|
+
let source = /** @type {string} */ (getTagDescription(tag));
|
|
337
|
+
const match = source.match(hasCaptionRegex);
|
|
338
|
+
|
|
339
|
+
if (captionRequired && (!match || !match[1].trim())) {
|
|
340
|
+
extraMessages.push({
|
|
341
|
+
line: 1 + commentLineCols[0] + (tag.line ?? tag.source[0].number),
|
|
342
|
+
column: commentLineCols[1] + 1,
|
|
343
|
+
severity: 2,
|
|
344
|
+
message: `@${targetTagName} error - Caption is expected for examples.`,
|
|
345
|
+
ruleId: 'jsdoc/example-missing-caption'
|
|
346
|
+
});
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
source = source.replace(hasCaptionRegex, '');
|
|
351
|
+
const [
|
|
352
|
+
lines,
|
|
353
|
+
cols,
|
|
354
|
+
] = match ? getLinesCols(match[0]) : [
|
|
355
|
+
0, 0,
|
|
356
|
+
];
|
|
357
|
+
|
|
358
|
+
if (exampleCodeRegex && !exampleCodeRegExp.test(source) ||
|
|
359
|
+
rejectExampleCodeRegex && rejectExampleCodeRegExp.test(source)
|
|
360
|
+
) {
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const sources = [];
|
|
365
|
+
let skipInit = false;
|
|
366
|
+
if (exampleCodeRegex) {
|
|
367
|
+
let nonJSPrefacingCols = 0;
|
|
368
|
+
let nonJSPrefacingLines = 0;
|
|
369
|
+
|
|
370
|
+
let startingIndex = 0;
|
|
371
|
+
let lastStringCount = 0;
|
|
372
|
+
|
|
373
|
+
let exampleCode;
|
|
374
|
+
exampleCodeRegExp.lastIndex = 0;
|
|
375
|
+
while ((exampleCode = exampleCodeRegExp.exec(source)) !== null) {
|
|
376
|
+
const {
|
|
377
|
+
index,
|
|
378
|
+
'0': n0,
|
|
379
|
+
'1': n1,
|
|
380
|
+
} = exampleCode;
|
|
381
|
+
|
|
382
|
+
// Count anything preceding user regex match (can affect line numbering)
|
|
383
|
+
const preMatch = source.slice(startingIndex, index);
|
|
384
|
+
|
|
385
|
+
const [
|
|
386
|
+
preMatchLines,
|
|
387
|
+
colDelta,
|
|
388
|
+
] = getLinesCols(preMatch);
|
|
389
|
+
|
|
390
|
+
let nonJSPreface;
|
|
391
|
+
let nonJSPrefaceLineCount;
|
|
392
|
+
if (n1) {
|
|
393
|
+
const idx = n0.indexOf(n1);
|
|
394
|
+
nonJSPreface = n0.slice(0, idx);
|
|
395
|
+
nonJSPrefaceLineCount = countChars(nonJSPreface, '\n');
|
|
396
|
+
} else {
|
|
397
|
+
nonJSPreface = '';
|
|
398
|
+
nonJSPrefaceLineCount = 0;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
nonJSPrefacingLines += lastStringCount + preMatchLines + nonJSPrefaceLineCount;
|
|
402
|
+
|
|
403
|
+
// Ignore `preMatch` delta if newlines here
|
|
404
|
+
if (nonJSPrefaceLineCount) {
|
|
405
|
+
const charsInLastLine = nonJSPreface.slice(nonJSPreface.lastIndexOf('\n') + 1).length;
|
|
406
|
+
|
|
407
|
+
nonJSPrefacingCols += charsInLastLine;
|
|
408
|
+
} else {
|
|
409
|
+
nonJSPrefacingCols += colDelta + nonJSPreface.length;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const string = n1 || n0;
|
|
413
|
+
sources.push({
|
|
414
|
+
nonJSPrefacingCols,
|
|
415
|
+
nonJSPrefacingLines,
|
|
416
|
+
string,
|
|
417
|
+
});
|
|
418
|
+
startingIndex = exampleCodeRegExp.lastIndex;
|
|
419
|
+
lastStringCount = countChars(string, '\n');
|
|
420
|
+
if (!exampleCodeRegExp.global) {
|
|
421
|
+
break;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
skipInit = true;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
checkSource({
|
|
429
|
+
cols,
|
|
430
|
+
lines,
|
|
431
|
+
skipInit,
|
|
432
|
+
source,
|
|
433
|
+
sources,
|
|
434
|
+
tag,
|
|
435
|
+
targetTagName,
|
|
436
|
+
...matchingFilenameInfo,
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
return textsAndFileNames;
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
// See https://eslint.org/docs/latest/extend/plugins#processors-in-plugins
|
|
444
|
+
// See https://eslint.org/docs/latest/extend/custom-processors
|
|
445
|
+
// From https://github.com/eslint/eslint/issues/14745#issuecomment-869457265
|
|
446
|
+
/*
|
|
447
|
+
{
|
|
448
|
+
"files": ["*.js", "*.ts"],
|
|
449
|
+
"processor": "jsdoc/example" // a pretended value here
|
|
450
|
+
},
|
|
451
|
+
{
|
|
452
|
+
"files": [
|
|
453
|
+
"*.js/*_jsdoc-example.js",
|
|
454
|
+
"*.ts/*_jsdoc-example.js",
|
|
455
|
+
"*.js/*_jsdoc-example.ts"
|
|
456
|
+
],
|
|
457
|
+
"rules": {
|
|
458
|
+
// specific rules for examples in jsdoc only here
|
|
459
|
+
// And other rules for `.js` and `.ts` will also be enabled for them
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
*/
|
|
463
|
+
return {
|
|
464
|
+
meta: {
|
|
465
|
+
name: 'eslint-plugin-jsdoc/processor',
|
|
466
|
+
version,
|
|
467
|
+
},
|
|
468
|
+
processors: {
|
|
469
|
+
examples: {
|
|
470
|
+
meta: {
|
|
471
|
+
name: 'eslint-plugin-jsdoc/preprocessor',
|
|
472
|
+
version,
|
|
473
|
+
},
|
|
474
|
+
/**
|
|
475
|
+
* @param {string} text
|
|
476
|
+
* @param {string} filename
|
|
477
|
+
*/
|
|
478
|
+
preprocess (text, filename) {
|
|
479
|
+
try {
|
|
480
|
+
let ast;
|
|
481
|
+
|
|
482
|
+
// May be running a second time so catch and ignore
|
|
483
|
+
try {
|
|
484
|
+
ast = parser
|
|
485
|
+
// @ts-expect-error Ok
|
|
486
|
+
? parser.parseForESLint(text, {
|
|
487
|
+
ecmaVersion: 'latest',
|
|
488
|
+
sourceType,
|
|
489
|
+
comment: true
|
|
490
|
+
}).ast
|
|
491
|
+
: espree.parse(text, {
|
|
492
|
+
ecmaVersion: 'latest',
|
|
493
|
+
sourceType,
|
|
494
|
+
comment: true
|
|
495
|
+
});
|
|
496
|
+
} catch (err) {
|
|
497
|
+
return [text];
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/** @type {[number, number][]} */
|
|
501
|
+
const commentLineCols = [];
|
|
502
|
+
const jsdocComments = /** @type {import('estree').Comment[]} */ (
|
|
503
|
+
/**
|
|
504
|
+
* @type {import('estree').Program & {
|
|
505
|
+
* comments?: import('estree').Comment[]
|
|
506
|
+
* }}
|
|
507
|
+
*/
|
|
508
|
+
(ast).comments
|
|
509
|
+
).filter((comment) => {
|
|
510
|
+
return (/^\*\s/u).test(comment.value);
|
|
511
|
+
}).map((comment) => {
|
|
512
|
+
/* c8 ignore next -- Unsupporting processors only? */
|
|
513
|
+
const [start] = comment.range ?? [];
|
|
514
|
+
const textToStart = text.slice(0, start);
|
|
515
|
+
|
|
516
|
+
const [lines, cols] = getLinesCols(textToStart);
|
|
517
|
+
|
|
518
|
+
// const lines = [...textToStart.matchAll(/\n/gu)].length
|
|
519
|
+
// const lastLinePos = textToStart.lastIndexOf('\n');
|
|
520
|
+
// const cols = lastLinePos === -1
|
|
521
|
+
// ? 0
|
|
522
|
+
// : textToStart.slice(lastLinePos).length;
|
|
523
|
+
commentLineCols.push([lines, cols]);
|
|
524
|
+
return parseComment(comment);
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
return [
|
|
528
|
+
text,
|
|
529
|
+
...jsdocComments.flatMap((jsdoc, idx) => {
|
|
530
|
+
return getTextsAndFileNames(
|
|
531
|
+
jsdoc,
|
|
532
|
+
filename,
|
|
533
|
+
commentLineCols[idx]
|
|
534
|
+
);
|
|
535
|
+
}).filter(Boolean)
|
|
536
|
+
];
|
|
537
|
+
/* c8 ignore next 3 */
|
|
538
|
+
} catch (err) {
|
|
539
|
+
console.log('err', filename, err);
|
|
540
|
+
}
|
|
541
|
+
},
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* @param {import('eslint').Linter.LintMessage[][]} messages
|
|
545
|
+
* @param {string} filename
|
|
546
|
+
*/
|
|
547
|
+
postprocess ([jsMessages, ...messages], filename) {
|
|
548
|
+
messages.forEach((message, idx) => {
|
|
549
|
+
const {
|
|
550
|
+
targetTagName,
|
|
551
|
+
codeStartLine,
|
|
552
|
+
codeStartCol,
|
|
553
|
+
nonJSPrefacingCols,
|
|
554
|
+
commentLineCols
|
|
555
|
+
} = otherInfo[idx];
|
|
556
|
+
|
|
557
|
+
message.forEach((msg) => {
|
|
558
|
+
const {
|
|
559
|
+
message,
|
|
560
|
+
ruleId,
|
|
561
|
+
severity,
|
|
562
|
+
fatal,
|
|
563
|
+
line,
|
|
564
|
+
column,
|
|
565
|
+
endColumn,
|
|
566
|
+
endLine,
|
|
567
|
+
|
|
568
|
+
// Todo: Make fixable
|
|
569
|
+
// fix
|
|
570
|
+
// fix: {range: [number, number], text: string}
|
|
571
|
+
// suggestions: {desc: , messageId:, fix: }[],
|
|
572
|
+
} = msg;
|
|
573
|
+
|
|
574
|
+
const [codeCtxLine, codeCtxColumn] = commentLineCols;
|
|
575
|
+
const startLine = codeCtxLine + codeStartLine + line;
|
|
576
|
+
const startCol = 1 + // Seems to need one more now
|
|
577
|
+
codeCtxColumn + codeStartCol + (
|
|
578
|
+
// This might not work for line 0, but line 0 is unlikely for examples
|
|
579
|
+
line <= 1 ? nonJSPrefacingCols + firstLinePrefixLength : preTagSpaceLength
|
|
580
|
+
) + column;
|
|
581
|
+
|
|
582
|
+
msg.message = '@' + targetTagName + ' ' + (severity === 2 ? 'error' : 'warning') +
|
|
583
|
+
(ruleId ? ' (' + ruleId + ')' : '') + ': ' +
|
|
584
|
+
(fatal ? 'Fatal: ' : '') +
|
|
585
|
+
message;
|
|
586
|
+
msg.line = startLine;
|
|
587
|
+
msg.column = startCol;
|
|
588
|
+
msg.endLine = endLine ? startLine + endLine : startLine;
|
|
589
|
+
// added `- column` to offset what `endColumn` already seemed to include
|
|
590
|
+
msg.endColumn = endColumn ? startCol - column + endColumn : startCol;
|
|
591
|
+
});
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
const ret = [...jsMessages].concat(...messages, ...extraMessages);
|
|
595
|
+
extraMessages = [];
|
|
596
|
+
return ret;
|
|
597
|
+
},
|
|
598
|
+
supportsAutofix: true
|
|
599
|
+
},
|
|
600
|
+
},
|
|
601
|
+
};
|
|
602
|
+
};
|
package/src/index.js
CHANGED
|
@@ -55,6 +55,8 @@ import tagLines from './rules/tagLines.js';
|
|
|
55
55
|
import textEscaping from './rules/textEscaping.js';
|
|
56
56
|
import validTypes from './rules/validTypes.js';
|
|
57
57
|
|
|
58
|
+
import { getJsdocProcessorPlugin } from './getJsdocProcessorPlugin.js';
|
|
59
|
+
|
|
58
60
|
/**
|
|
59
61
|
* @type {import('eslint').ESLint.Plugin & {
|
|
60
62
|
* configs: Record<
|
|
@@ -274,4 +276,108 @@ index.configs['flat/recommended-typescript-error'] = createRecommendedTypeScript
|
|
|
274
276
|
index.configs['flat/recommended-typescript-flavor'] = createRecommendedTypeScriptFlavorRuleset('warn', 'flat/recommended-typescript-flavor');
|
|
275
277
|
index.configs['flat/recommended-typescript-flavor-error'] = createRecommendedTypeScriptFlavorRuleset('error', 'flat/recommended-typescript-flavor-error');
|
|
276
278
|
|
|
279
|
+
index.configs.examples = /** @type {import('eslint').Linter.FlatConfig[]} */ ([
|
|
280
|
+
{
|
|
281
|
+
name: 'jsdoc/examples/processor',
|
|
282
|
+
files: ['**/*.js'],
|
|
283
|
+
plugins: {
|
|
284
|
+
examples: getJsdocProcessorPlugin()
|
|
285
|
+
},
|
|
286
|
+
processor: 'examples/examples',
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
name: 'jsdoc/examples/rules',
|
|
290
|
+
files: ['**/*.md/*.js'],
|
|
291
|
+
rules: {
|
|
292
|
+
// "always" newline rule at end unlikely in sample code
|
|
293
|
+
'eol-last': 0,
|
|
294
|
+
|
|
295
|
+
// Wouldn't generally expect example paths to resolve relative to JS file
|
|
296
|
+
'import/no-unresolved': 0,
|
|
297
|
+
|
|
298
|
+
// Snippets likely too short to always include import/export info
|
|
299
|
+
'import/unambiguous': 0,
|
|
300
|
+
|
|
301
|
+
'jsdoc/require-file-overview': 0,
|
|
302
|
+
|
|
303
|
+
// The end of a multiline comment would end the comment the example is in.
|
|
304
|
+
'jsdoc/require-jsdoc': 0,
|
|
305
|
+
|
|
306
|
+
// Unlikely to have inadvertent debugging within examples
|
|
307
|
+
'no-console': 0,
|
|
308
|
+
|
|
309
|
+
// Often wish to start `@example` code after newline; also may use
|
|
310
|
+
// empty lines for spacing
|
|
311
|
+
'no-multiple-empty-lines': 0,
|
|
312
|
+
|
|
313
|
+
// Many variables in examples will be `undefined`
|
|
314
|
+
'no-undef': 0,
|
|
315
|
+
|
|
316
|
+
// Common to define variables for clarity without always using them
|
|
317
|
+
'no-unused-vars': 0,
|
|
318
|
+
|
|
319
|
+
// See import/no-unresolved
|
|
320
|
+
'node/no-missing-import': 0,
|
|
321
|
+
'node/no-missing-require': 0,
|
|
322
|
+
|
|
323
|
+
// Can generally look nicer to pad a little even if code imposes more stringency
|
|
324
|
+
'padded-blocks': 0,
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
]);
|
|
328
|
+
|
|
329
|
+
index.configs['default-expressions'] = /** @type {import('eslint').Linter.FlatConfig[]} */ ([
|
|
330
|
+
{
|
|
331
|
+
files: ['**/*.js'],
|
|
332
|
+
name: 'jsdoc/default-expressions/processor',
|
|
333
|
+
plugins: {
|
|
334
|
+
examples: getJsdocProcessorPlugin({
|
|
335
|
+
checkDefaults: true,
|
|
336
|
+
checkParams: true,
|
|
337
|
+
checkProperties: true
|
|
338
|
+
})
|
|
339
|
+
},
|
|
340
|
+
processor: 'examples/examples'
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
name: 'jsdoc/default-expressions/rules',
|
|
344
|
+
files: ['**/*.jsdoc-defaults', '**/*.jsdoc-params', '**/*.jsdoc-properties'],
|
|
345
|
+
rules: {
|
|
346
|
+
...index.configs.examples[1].rules,
|
|
347
|
+
'chai-friendly/no-unused-expressions': 0,
|
|
348
|
+
'no-empty-function': 0,
|
|
349
|
+
'no-new': 0,
|
|
350
|
+
'no-unused-expressions': 0,
|
|
351
|
+
quotes: [
|
|
352
|
+
'error', 'double',
|
|
353
|
+
],
|
|
354
|
+
semi: [
|
|
355
|
+
'error', 'never',
|
|
356
|
+
],
|
|
357
|
+
strict: 0
|
|
358
|
+
},
|
|
359
|
+
}
|
|
360
|
+
]);
|
|
361
|
+
|
|
362
|
+
index.configs['examples-and-default-expressions'] = /** @type {import('eslint').Linter.FlatConfig[]} */ ([
|
|
363
|
+
{
|
|
364
|
+
name: 'jsdoc/examples-and-default-expressions',
|
|
365
|
+
plugins: {
|
|
366
|
+
examples: getJsdocProcessorPlugin({
|
|
367
|
+
checkDefaults: true,
|
|
368
|
+
checkParams: true,
|
|
369
|
+
checkProperties: true
|
|
370
|
+
})
|
|
371
|
+
},
|
|
372
|
+
},
|
|
373
|
+
...index.configs.examples.map((config) => {
|
|
374
|
+
delete config.plugins;
|
|
375
|
+
return config;
|
|
376
|
+
}),
|
|
377
|
+
...index.configs['default-expressions'].map((config) => {
|
|
378
|
+
delete config.plugins;
|
|
379
|
+
return config;
|
|
380
|
+
})
|
|
381
|
+
]);
|
|
382
|
+
|
|
277
383
|
export default index;
|