| "use strict"; |
| |
| Object.defineProperty(exports, "__esModule", { |
| value: true |
| }); |
| exports.getJsdocProcessorPlugin = void 0; |
| var _jsdocUtils = require("./jsdocUtils.cjs"); |
| var _jsdoccomment = require("@es-joy/jsdoccomment"); |
| var espree = _interopRequireWildcard(require("espree")); |
| var _htmlEntities = require("html-entities"); |
| var _nodeFs = require("node:fs"); |
| var _nodePath = require("node:path"); |
| function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); } |
| /** |
| * @import { |
| * Integer, |
| * JsdocBlockWithInline, |
| * } from './iterateJsdoc.js'; |
| * @import { |
| * ESLint, |
| * Linter, |
| * } from 'eslint'; |
| */ |
| |
| const { |
| version |
| } = JSON.parse((0, _nodeFs.readFileSync)((0, _nodePath.join)(__dirname, '../package.json'), 'utf8')); |
| |
| // const zeroBasedLineIndexAdjust = -1; |
| const likelyNestedJSDocIndentSpace = 1; |
| const preTagSpaceLength = 1; |
| |
| // If a space is present, we should ignore it |
| const firstLinePrefixLength = preTagSpaceLength; |
| const hasCaptionRegex = /^\s*<caption>([\s\S]*?)<\/caption>/v; |
| |
| /** |
| * @param {string} str |
| * @returns {string} |
| */ |
| const escapeStringRegexp = str => { |
| return str.replaceAll(/[.*+?^$\{\}\(\)\|\[\]\\]/gv, '\\$&'); |
| }; |
| |
| /** |
| * @param {string} str |
| * @param {string} ch |
| * @returns {Integer} |
| */ |
| const countChars = (str, ch) => { |
| return (str.match(new RegExp(escapeStringRegexp(ch), 'gv')) || []).length; |
| }; |
| |
| /** |
| * @param {string} text |
| * @returns {[ |
| * Integer, |
| * Integer |
| * ]} |
| */ |
| const getLinesCols = text => { |
| const matchLines = countChars(text, '\n'); |
| const colDelta = matchLines ? text.slice(text.lastIndexOf('\n') + 1).length : text.length; |
| return [matchLines, colDelta]; |
| }; |
| |
| /** |
| * @typedef {number} Integer |
| */ |
| /** |
| * @typedef {object} JsdocProcessorOptions |
| * @property {boolean} [captionRequired] Require captions for example tags |
| * @property {Integer} [paddedIndent] See docs |
| * @property {boolean} [checkDefaults] See docs |
| * @property {boolean} [checkParams] See docs |
| * @property {boolean} [checkExamples] See docs |
| * @property {boolean} [checkProperties] See docs |
| * @property {string} [matchingFileName] See docs |
| * @property {string} [matchingFileNameDefaults] See docs |
| * @property {string} [matchingFileNameParams] See docs |
| * @property {string} [matchingFileNameProperties] See docs |
| * @property {string|RegExp} [exampleCodeRegex] See docs |
| * @property {string|RegExp} [rejectExampleCodeRegex] See docs |
| * @property {string[]} [allowedLanguagesToProcess] See docs |
| * @property {"script"|"module"} [sourceType] See docs |
| * @property {import('eslint').Linter.ESTreeParser|import('eslint').Linter.NonESTreeParser} [parser] See docs |
| */ |
| |
| /** |
| * We use a function for the ability of the user to pass in a config, but |
| * without requiring all users of the plugin to do so. |
| * @param {JsdocProcessorOptions} [options] |
| * @returns {ESLint.Plugin} |
| */ |
| const getJsdocProcessorPlugin = (options = {}) => { |
| /** |
| * @typedef {{ |
| * text: string, |
| * filename: string|null|undefined |
| * }} TextAndFileName |
| */ |
| |
| const { |
| allowedLanguagesToProcess = ['js', 'ts', 'javascript', 'typescript'], |
| captionRequired = false, |
| checkDefaults = false, |
| checkExamples = true, |
| checkParams = false, |
| checkProperties = false, |
| exampleCodeRegex = null, |
| matchingFileName = null, |
| matchingFileNameDefaults = null, |
| matchingFileNameParams = null, |
| matchingFileNameProperties = null, |
| paddedIndent = 0, |
| parser = undefined, |
| rejectExampleCodeRegex = null, |
| sourceType = 'module' |
| } = options; |
| |
| /** @type {RegExp} */ |
| let exampleCodeRegExp; |
| /** @type {RegExp} */ |
| let rejectExampleCodeRegExp; |
| if (exampleCodeRegex) { |
| exampleCodeRegExp = typeof exampleCodeRegex === 'string' ? (0, _jsdocUtils.getRegexFromString)(exampleCodeRegex) : exampleCodeRegex; |
| } |
| if (rejectExampleCodeRegex) { |
| rejectExampleCodeRegExp = typeof rejectExampleCodeRegex === 'string' ? (0, _jsdocUtils.getRegexFromString)(rejectExampleCodeRegex) : rejectExampleCodeRegex; |
| } |
| |
| /** |
| * @type {{ |
| * targetTagName: string, |
| * ext: string, |
| * codeStartLine: number, |
| * codeStartCol: number, |
| * nonJSPrefacingCols: number, |
| * commentLineCols: [number, number] |
| * }[]} |
| */ |
| const otherInfo = []; |
| |
| /** @type {import('eslint').Linter.LintMessage[]} */ |
| let extraMessages = []; |
| |
| /** |
| * @param {JsdocBlockWithInline} jsdoc |
| * @param {string} jsFileName |
| * @param {[number, number]} commentLineCols |
| */ |
| const getTextsAndFileNames = (jsdoc, jsFileName, commentLineCols) => { |
| /** |
| * @type {TextAndFileName[]} |
| */ |
| const textsAndFileNames = []; |
| |
| /** |
| * @param {{ |
| * filename: string|null, |
| * defaultFileName: string|undefined, |
| * source: string, |
| * targetTagName: string, |
| * rules?: import('eslint').Linter.RulesRecord|undefined, |
| * lines?: Integer, |
| * cols?: Integer, |
| * skipInit?: boolean, |
| * ext: string, |
| * sources?: { |
| * nonJSPrefacingCols: Integer, |
| * nonJSPrefacingLines: Integer, |
| * string: string, |
| * }[], |
| * tag?: import('comment-parser').Spec & { |
| * line?: Integer, |
| * }|{ |
| * line: Integer, |
| * } |
| * }} cfg |
| */ |
| const checkSource = ({ |
| cols = 0, |
| defaultFileName, |
| ext, |
| filename, |
| lines = 0, |
| skipInit, |
| source, |
| sources = [], |
| tag = { |
| line: 0 |
| }, |
| targetTagName |
| }) => { |
| if (!skipInit) { |
| sources.push({ |
| nonJSPrefacingCols: cols, |
| nonJSPrefacingLines: lines, |
| string: source |
| }); |
| } |
| |
| /** |
| * @param {{ |
| * nonJSPrefacingCols: Integer, |
| * nonJSPrefacingLines: Integer, |
| * string: string |
| * }} cfg |
| */ |
| const addSourceInfo = function ({ |
| nonJSPrefacingCols, |
| nonJSPrefacingLines, |
| string |
| }) { |
| const src = paddedIndent ? string.replaceAll(new RegExp(`(^|\n) {${paddedIndent}}(?!$)`, 'gv'), '\n') : string; |
| |
| // Programmatic ESLint API: https://eslint.org/docs/developer-guide/nodejs-api |
| const file = filename || defaultFileName; |
| if (!('line' in tag)) { |
| tag.line = tag.source[0].number; |
| } |
| |
| // NOTE: `tag.line` can be 0 if of form `/** @tag ... */` |
| const codeStartLine = |
| /** |
| * @type {import('comment-parser').Spec & { |
| * line: Integer, |
| * }} |
| */ |
| tag.line + nonJSPrefacingLines; |
| const codeStartCol = likelyNestedJSDocIndentSpace; |
| textsAndFileNames.push({ |
| filename: file, |
| // See https://github.com/gajus/eslint-plugin-jsdoc/issues/710 |
| text: src.replaceAll(/(?<=\*)\\(?=\\*\/)/gv, '').replaceAll(/&([^\s;]+);/gv, (_, code) => { |
| // Dec |
| if (/^#\d+$/v.test(code)) { |
| return String.fromCodePoint(Number.parseInt(code.slice(1), 10)); |
| } |
| |
| // Hex |
| if (/^#x\d+$/v.test(code)) { |
| return String.fromCodePoint(Number.parseInt(code.slice(2), 16)); |
| } |
| return (0, _htmlEntities.decode)(_, { |
| level: 'html5' |
| }); |
| }) |
| }); |
| otherInfo.push({ |
| codeStartCol, |
| codeStartLine, |
| commentLineCols, |
| ext, |
| nonJSPrefacingCols, |
| targetTagName |
| }); |
| }; |
| for (const targetSource of sources) { |
| addSourceInfo(targetSource); |
| } |
| }; |
| |
| /** |
| * |
| * @param {string|null} filename |
| * @param {string} [ext] Since `eslint-plugin-markdown` v2, and |
| * ESLint 7, this is the default which other JS-fenced rules will used. |
| * Formerly "md" was the default. |
| * @returns {{ |
| * defaultFileName: string|undefined, |
| * filename: string|null, |
| * ext: string |
| * }} |
| */ |
| const getFilenameInfo = (filename, ext = 'md/*.js') => { |
| let defaultFileName; |
| if (!filename) { |
| if (typeof jsFileName === 'string' && jsFileName.includes('.')) { |
| defaultFileName = jsFileName.replace(/\.[^.]*$/v, `.${ext}`); |
| } else { |
| defaultFileName = `dummy.${ext}`; |
| } |
| } |
| return { |
| defaultFileName, |
| ext, |
| filename |
| }; |
| }; |
| if (checkDefaults) { |
| const filenameInfo = getFilenameInfo(matchingFileNameDefaults, 'jsdoc-defaults'); |
| (0, _jsdocUtils.forEachPreferredTag)(jsdoc, 'default', (tag, targetTagName) => { |
| if (!tag.description.trim()) { |
| return; |
| } |
| checkSource({ |
| source: `(${(0, _jsdocUtils.getTagDescription)(tag)})`, |
| targetTagName, |
| ...filenameInfo |
| }); |
| }); |
| } |
| if (checkParams) { |
| const filenameInfo = getFilenameInfo(matchingFileNameParams, 'jsdoc-params'); |
| (0, _jsdocUtils.forEachPreferredTag)(jsdoc, 'param', (tag, targetTagName) => { |
| if (!tag.default || !tag.default.trim()) { |
| return; |
| } |
| checkSource({ |
| source: `(${tag.default})`, |
| targetTagName, |
| ...filenameInfo |
| }); |
| }); |
| } |
| if (checkProperties) { |
| const filenameInfo = getFilenameInfo(matchingFileNameProperties, 'jsdoc-properties'); |
| (0, _jsdocUtils.forEachPreferredTag)(jsdoc, 'property', (tag, targetTagName) => { |
| if (!tag.default || !tag.default.trim()) { |
| return; |
| } |
| checkSource({ |
| source: `(${tag.default})`, |
| targetTagName, |
| ...filenameInfo |
| }); |
| }); |
| } |
| if (!checkExamples) { |
| return textsAndFileNames; |
| } |
| const tagName = /** @type {string} */(0, _jsdocUtils.getPreferredTagName)(jsdoc, { |
| tagName: 'example' |
| }); |
| if (!(0, _jsdocUtils.hasTag)(jsdoc, tagName)) { |
| return textsAndFileNames; |
| } |
| const matchingFilenameInfo = getFilenameInfo(matchingFileName); |
| (0, _jsdocUtils.forEachPreferredTag)(jsdoc, 'example', (tag, targetTagName) => { |
| let source = /** @type {string} */(0, _jsdocUtils.getTagDescription)(tag); |
| const match = source.match(hasCaptionRegex); |
| if (captionRequired && (!match || !match[1].trim())) { |
| extraMessages.push({ |
| column: commentLineCols[1] + 1, |
| line: 1 + commentLineCols[0] + (tag.line ?? tag.source[0].number), |
| message: `@${targetTagName} error - Caption is expected for examples.`, |
| ruleId: 'jsdoc/example-missing-caption', |
| severity: 2 |
| }); |
| return; |
| } |
| source = source.replace(hasCaptionRegex, ''); |
| const [lines, cols] = match ? getLinesCols(match[0]) : [0, 0]; |
| if (exampleCodeRegex && !exampleCodeRegExp.test(source) || rejectExampleCodeRegex && rejectExampleCodeRegExp.test(source)) { |
| return; |
| } |
| |
| // If `allowedLanguagesToProcess` is falsy, all languages should be processed. |
| if (allowedLanguagesToProcess) { |
| const matches = /^\s*```(?<language>\S+)([\s\S]*)```\s*$/v.exec(source); |
| if (matches?.groups && !allowedLanguagesToProcess.includes(matches.groups.language.toLowerCase())) { |
| return; |
| } |
| } |
| const sources = []; |
| let skipInit = false; |
| if (exampleCodeRegex) { |
| let nonJSPrefacingCols = 0; |
| let nonJSPrefacingLines = 0; |
| let startingIndex = 0; |
| let lastStringCount = 0; |
| let exampleCode; |
| exampleCodeRegExp.lastIndex = 0; |
| while ((exampleCode = exampleCodeRegExp.exec(source)) !== null) { |
| const { |
| '0': n0, |
| '1': n1, |
| index |
| } = exampleCode; |
| |
| // Count anything preceding user regex match (can affect line numbering) |
| const preMatch = source.slice(startingIndex, index); |
| const [preMatchLines, colDelta] = getLinesCols(preMatch); |
| let nonJSPreface; |
| let nonJSPrefaceLineCount; |
| if (n1) { |
| const idx = n0.indexOf(n1); |
| nonJSPreface = n0.slice(0, idx); |
| nonJSPrefaceLineCount = countChars(nonJSPreface, '\n'); |
| } else { |
| nonJSPreface = ''; |
| nonJSPrefaceLineCount = 0; |
| } |
| nonJSPrefacingLines += lastStringCount + preMatchLines + nonJSPrefaceLineCount; |
| |
| // Ignore `preMatch` delta if newlines here |
| if (nonJSPrefaceLineCount) { |
| const charsInLastLine = nonJSPreface.slice(nonJSPreface.lastIndexOf('\n') + 1).length; |
| nonJSPrefacingCols += charsInLastLine; |
| } else { |
| nonJSPrefacingCols += colDelta + nonJSPreface.length; |
| } |
| const string = n1 || n0; |
| sources.push({ |
| nonJSPrefacingCols, |
| nonJSPrefacingLines, |
| string |
| }); |
| startingIndex = exampleCodeRegExp.lastIndex; |
| lastStringCount = countChars(string, '\n'); |
| if (!exampleCodeRegExp.global) { |
| break; |
| } |
| } |
| skipInit = true; |
| } |
| checkSource({ |
| cols, |
| lines, |
| skipInit, |
| source, |
| sources, |
| tag, |
| targetTagName, |
| ...matchingFilenameInfo |
| }); |
| }); |
| return textsAndFileNames; |
| }; |
| |
| // See https://eslint.org/docs/latest/extend/plugins#processors-in-plugins |
| // See https://eslint.org/docs/latest/extend/custom-processors |
| // From https://github.com/eslint/eslint/issues/14745#issuecomment-869457265 |
| /* |
| { |
| "files": ["*.js", "*.ts"], |
| "processor": "jsdoc/example" // a pretended value here |
| }, |
| { |
| "files": [ |
| "*.js/*_jsdoc-example.js", |
| "*.ts/*_jsdoc-example.js", |
| "*.js/*_jsdoc-example.ts" |
| ], |
| "rules": { |
| // specific rules for examples in jsdoc only here |
| // And other rules for `.js` and `.ts` will also be enabled for them |
| } |
| } |
| */ |
| return { |
| meta: { |
| name: 'eslint-plugin-jsdoc/processor', |
| version |
| }, |
| processors: { |
| examples: { |
| meta: { |
| name: 'eslint-plugin-jsdoc/preprocessor', |
| version |
| }, |
| /** |
| * @param {import('eslint').Linter.LintMessage[][]} messages |
| * @param {string} filename |
| */ |
| postprocess([jsMessages, ...messages |
| // eslint-disable-next-line no-unused-vars -- Placeholder |
| ], filename) { |
| for (const [idx, message] of messages.entries()) { |
| const { |
| codeStartCol, |
| codeStartLine, |
| commentLineCols, |
| nonJSPrefacingCols, |
| targetTagName |
| } = otherInfo[idx]; |
| for (const msg of message) { |
| const { |
| column, |
| endColumn, |
| endLine, |
| fatal, |
| line, |
| message: messageText, |
| ruleId, |
| severity |
| |
| // Todo: Make fixable |
| // fix |
| // fix: {range: [number, number], text: string} |
| // suggestions: {desc: , messageId:, fix: }[], |
| } = msg; |
| delete msg.fix; |
| const [codeCtxLine, codeCtxColumn] = commentLineCols; |
| const startLine = codeCtxLine + codeStartLine + line; |
| |
| // Seems to need one more now |
| const startCol = 1 + codeCtxColumn + codeStartCol + ( |
| // This might not work for line 0, but line 0 is unlikely for examples |
| line <= 1 ? nonJSPrefacingCols + firstLinePrefixLength : preTagSpaceLength) + column; |
| msg.message = '@' + targetTagName + ' ' + (severity === 2 ? 'error' : 'warning') + (ruleId ? ' (' + ruleId + ')' : '') + ': ' + (fatal ? 'Fatal: ' : '') + messageText; |
| msg.line = startLine; |
| msg.column = startCol; |
| msg.endLine = endLine ? startLine + endLine : startLine; |
| // added `- column` to offset what `endColumn` already seemed to include |
| msg.endColumn = endColumn ? startCol - column + endColumn : startCol; |
| } |
| } |
| const ret = [...jsMessages].concat(...messages, ...extraMessages); |
| extraMessages = []; |
| return ret; |
| }, |
| /** |
| * @param {string} text |
| * @param {string} filename |
| * @returns {(string | Linter.ProcessorFile)[]} |
| */ |
| preprocess(text, filename) { |
| try { |
| let ast; |
| |
| // May be running a second time so catch and ignore |
| try { |
| ast = parser ? |
| // @ts-expect-error Should be present |
| parser.parseForESLint(text, { |
| comment: true, |
| ecmaVersion: 'latest', |
| sourceType |
| }).ast : espree.parse(text, { |
| comment: true, |
| ecmaVersion: 'latest', |
| sourceType |
| }); |
| } catch { |
| return [text]; |
| } |
| |
| /** @type {[number, number][]} */ |
| const commentLineCols = []; |
| const jsdocComments = /** @type {import('estree').Comment[]} */( |
| /** |
| * @type {import('estree').Program & { |
| * comments?: import('estree').Comment[] |
| * }} |
| */ |
| ast.comments).filter(comment => { |
| return /^\*\s/v.test(comment.value); |
| }).map(comment => { |
| const [start |
| /* c8 ignore next -- Unsupporting processors only? */] = comment.range ?? []; |
| const textToStart = text.slice(0, start); |
| const [lines, cols] = getLinesCols(textToStart); |
| |
| // const lines = [...textToStart.matchAll(/\n/gv)].length |
| // const lastLinePos = textToStart.lastIndexOf('\n'); |
| // const cols = lastLinePos === -1 |
| // ? 0 |
| // : textToStart.slice(lastLinePos).length; |
| commentLineCols.push([lines, cols]); |
| return (0, _jsdoccomment.parseComment)(comment); |
| }); |
| return [text, ...jsdocComments.flatMap((jsdoc, idx) => { |
| return getTextsAndFileNames(jsdoc, filename, commentLineCols[idx]); |
| }).filter( |
| /** |
| * @param {TextAndFileName} file |
| * @returns {file is Linter.ProcessorFile} |
| */ |
| file => { |
| return file !== null && file !== undefined; |
| })]; |
| /* c8 ignore next 6 */ |
| } catch (error) { |
| // eslint-disable-next-line no-console -- Debugging |
| console.log('err', filename, error); |
| } |
| return []; |
| }, |
| supportsAutofix: true |
| } |
| } |
| }; |
| }; |
| exports.getJsdocProcessorPlugin = getJsdocProcessorPlugin; |
| //# sourceMappingURL=getJsdocProcessorPlugin.cjs.map |