checkExamples.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", {
  3. value: true
  4. });
  5. exports.default = void 0;
  6. var _iterateJsdoc = _interopRequireDefault(require("../iterateJsdoc.js"));
  7. var _eslint = _interopRequireWildcard(require("eslint"));
  8. var _semver = _interopRequireDefault(require("semver"));
  9. function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
  10. function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
  11. function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
  12. // Todo: When replace `CLIEngine` with `ESLint` when feature set complete per https://github.com/eslint/eslint/issues/14745
  13. // https://github.com/eslint/eslint/blob/master/docs/user-guide/migrating-to-7.0.0.md#-the-cliengine-class-has-been-deprecated
  14. const {
  15. // @ts-expect-error Older ESLint
  16. CLIEngine
  17. } = _eslint.default;
  18. const zeroBasedLineIndexAdjust = -1;
  19. const likelyNestedJSDocIndentSpace = 1;
  20. const preTagSpaceLength = 1;
  21. // If a space is present, we should ignore it
  22. const firstLinePrefixLength = preTagSpaceLength;
  23. const hasCaptionRegex = /^\s*<caption>([\s\S]*?)<\/caption>/u;
  24. /**
  25. * @param {string} str
  26. * @returns {string}
  27. */
  28. const escapeStringRegexp = str => {
  29. return str.replaceAll(/[.*+?^${}()|[\]\\]/gu, '\\$&');
  30. };
  31. /**
  32. * @param {string} str
  33. * @param {string} ch
  34. * @returns {import('../iterateJsdoc.js').Integer}
  35. */
  36. const countChars = (str, ch) => {
  37. return (str.match(new RegExp(escapeStringRegexp(ch), 'gu')) || []).length;
  38. };
  39. /** @type {import('eslint').Linter.RulesRecord} */
  40. const defaultMdRules = {
  41. // "always" newline rule at end unlikely in sample code
  42. 'eol-last': 0,
  43. // Wouldn't generally expect example paths to resolve relative to JS file
  44. 'import/no-unresolved': 0,
  45. // Snippets likely too short to always include import/export info
  46. 'import/unambiguous': 0,
  47. 'jsdoc/require-file-overview': 0,
  48. // The end of a multiline comment would end the comment the example is in.
  49. 'jsdoc/require-jsdoc': 0,
  50. // Unlikely to have inadvertent debugging within examples
  51. 'no-console': 0,
  52. // Often wish to start `@example` code after newline; also may use
  53. // empty lines for spacing
  54. 'no-multiple-empty-lines': 0,
  55. // Many variables in examples will be `undefined`
  56. 'no-undef': 0,
  57. // Common to define variables for clarity without always using them
  58. 'no-unused-vars': 0,
  59. // See import/no-unresolved
  60. 'node/no-missing-import': 0,
  61. 'node/no-missing-require': 0,
  62. // Can generally look nicer to pad a little even if code imposes more stringency
  63. 'padded-blocks': 0
  64. };
  65. /** @type {import('eslint').Linter.RulesRecord} */
  66. const defaultExpressionRules = {
  67. ...defaultMdRules,
  68. 'chai-friendly/no-unused-expressions': 'off',
  69. 'no-empty-function': 'off',
  70. 'no-new': 'off',
  71. 'no-unused-expressions': 'off',
  72. quotes: ['error', 'double'],
  73. semi: ['error', 'never'],
  74. strict: 'off'
  75. };
  76. /**
  77. * @param {string} text
  78. * @returns {[
  79. * import('../iterateJsdoc.js').Integer,
  80. * import('../iterateJsdoc.js').Integer
  81. * ]}
  82. */
  83. const getLinesCols = text => {
  84. const matchLines = countChars(text, '\n');
  85. const colDelta = matchLines ? text.slice(text.lastIndexOf('\n') + 1).length : text.length;
  86. return [matchLines, colDelta];
  87. };
  88. var _default = exports.default = (0, _iterateJsdoc.default)(({
  89. report,
  90. utils,
  91. context,
  92. globalState
  93. }) => {
  94. if (_semver.default.gte(_eslint.ESLint.version, '8.0.0')) {
  95. report('This rule cannot yet be supported for ESLint 8; you ' + 'should either downgrade to ESLint 7 or disable this rule. The ' + 'possibility for ESLint 8 support is being tracked at https://github.com/eslint/eslint/issues/14745', null, {
  96. column: 1,
  97. line: 1
  98. });
  99. return;
  100. }
  101. if (!globalState.has('checkExamples-matchingFileName')) {
  102. globalState.set('checkExamples-matchingFileName', new Map());
  103. }
  104. const matchingFileNameMap = /** @type {Map<string, string>} */
  105. globalState.get('checkExamples-matchingFileName');
  106. const options = context.options[0] || {};
  107. let {
  108. exampleCodeRegex = null,
  109. rejectExampleCodeRegex = null
  110. } = options;
  111. const {
  112. checkDefaults = false,
  113. checkParams = false,
  114. checkProperties = false,
  115. noDefaultExampleRules = false,
  116. checkEslintrc = true,
  117. matchingFileName = null,
  118. matchingFileNameDefaults = null,
  119. matchingFileNameParams = null,
  120. matchingFileNameProperties = null,
  121. paddedIndent = 0,
  122. baseConfig = {},
  123. configFile,
  124. allowInlineConfig = true,
  125. reportUnusedDisableDirectives = true,
  126. captionRequired = false
  127. } = options;
  128. // Make this configurable?
  129. /**
  130. * @type {never[]}
  131. */
  132. const rulePaths = [];
  133. const mdRules = noDefaultExampleRules ? undefined : defaultMdRules;
  134. const expressionRules = noDefaultExampleRules ? undefined : defaultExpressionRules;
  135. if (exampleCodeRegex) {
  136. exampleCodeRegex = utils.getRegexFromString(exampleCodeRegex);
  137. }
  138. if (rejectExampleCodeRegex) {
  139. rejectExampleCodeRegex = utils.getRegexFromString(rejectExampleCodeRegex);
  140. }
  141. /**
  142. * @param {{
  143. * filename: string,
  144. * defaultFileName: string|undefined,
  145. * source: string,
  146. * targetTagName: string,
  147. * rules?: import('eslint').Linter.RulesRecord|undefined,
  148. * lines?: import('../iterateJsdoc.js').Integer,
  149. * cols?: import('../iterateJsdoc.js').Integer,
  150. * skipInit?: boolean,
  151. * sources?: {
  152. * nonJSPrefacingCols: import('../iterateJsdoc.js').Integer,
  153. * nonJSPrefacingLines: import('../iterateJsdoc.js').Integer,
  154. * string: string,
  155. * }[],
  156. * tag?: import('comment-parser').Spec & {
  157. * line?: import('../iterateJsdoc.js').Integer,
  158. * }|{
  159. * line: import('../iterateJsdoc.js').Integer,
  160. * }
  161. * }} cfg
  162. */
  163. const checkSource = ({
  164. filename,
  165. defaultFileName,
  166. rules = expressionRules,
  167. lines = 0,
  168. cols = 0,
  169. skipInit,
  170. source,
  171. targetTagName,
  172. sources = [],
  173. tag = {
  174. line: 0
  175. }
  176. }) => {
  177. if (!skipInit) {
  178. sources.push({
  179. nonJSPrefacingCols: cols,
  180. nonJSPrefacingLines: lines,
  181. string: source
  182. });
  183. }
  184. // Todo: Make fixable
  185. /**
  186. * @param {{
  187. * nonJSPrefacingCols: import('../iterateJsdoc.js').Integer,
  188. * nonJSPrefacingLines: import('../iterateJsdoc.js').Integer,
  189. * string: string
  190. * }} cfg
  191. */
  192. const checkRules = function ({
  193. nonJSPrefacingCols,
  194. nonJSPrefacingLines,
  195. string
  196. }) {
  197. const cliConfig = {
  198. allowInlineConfig,
  199. baseConfig,
  200. configFile,
  201. reportUnusedDisableDirectives,
  202. rulePaths,
  203. rules,
  204. useEslintrc: checkEslintrc
  205. };
  206. const cliConfigStr = JSON.stringify(cliConfig);
  207. const src = paddedIndent ? string.replaceAll(new RegExp(`(^|\n) {${paddedIndent}}(?!$)`, 'gu'), '\n') : string;
  208. // Programmatic ESLint API: https://eslint.org/docs/developer-guide/nodejs-api
  209. const fileNameMapKey = filename ? 'a' + cliConfigStr + filename : 'b' + cliConfigStr + defaultFileName;
  210. const file = filename || defaultFileName;
  211. let cliFile;
  212. if (matchingFileNameMap.has(fileNameMapKey)) {
  213. cliFile = matchingFileNameMap.get(fileNameMapKey);
  214. } else {
  215. const cli = new CLIEngine(cliConfig);
  216. let config;
  217. if (filename || checkEslintrc) {
  218. config = cli.getConfigForFile(file);
  219. }
  220. // We need a new instance to ensure that the rules that may only
  221. // be available to `file` (if it has its own `.eslintrc`),
  222. // will be defined.
  223. cliFile = new CLIEngine({
  224. allowInlineConfig,
  225. baseConfig: {
  226. ...baseConfig,
  227. ...config
  228. },
  229. configFile,
  230. reportUnusedDisableDirectives,
  231. rulePaths,
  232. rules,
  233. useEslintrc: false
  234. });
  235. matchingFileNameMap.set(fileNameMapKey, cliFile);
  236. }
  237. const {
  238. results: [{
  239. messages
  240. }]
  241. } = cliFile.executeOnText(src);
  242. if (!('line' in tag)) {
  243. tag.line = tag.source[0].number;
  244. }
  245. // NOTE: `tag.line` can be 0 if of form `/** @tag ... */`
  246. const codeStartLine =
  247. /**
  248. * @type {import('comment-parser').Spec & {
  249. * line: import('../iterateJsdoc.js').Integer,
  250. * }}
  251. */
  252. tag.line + nonJSPrefacingLines;
  253. const codeStartCol = likelyNestedJSDocIndentSpace;
  254. for (const {
  255. message,
  256. line,
  257. column,
  258. severity,
  259. ruleId
  260. } of messages) {
  261. const startLine = codeStartLine + line + zeroBasedLineIndexAdjust;
  262. const startCol = codeStartCol + (
  263. // This might not work for line 0, but line 0 is unlikely for examples
  264. line <= 1 ? nonJSPrefacingCols + firstLinePrefixLength : preTagSpaceLength) + column;
  265. report('@' + targetTagName + ' ' + (severity === 2 ? 'error' : 'warning') + (ruleId ? ' (' + ruleId + ')' : '') + ': ' + message, null, {
  266. column: startCol,
  267. line: startLine
  268. });
  269. }
  270. };
  271. for (const targetSource of sources) {
  272. checkRules(targetSource);
  273. }
  274. };
  275. /**
  276. *
  277. * @param {string} filename
  278. * @param {string} [ext] Since `eslint-plugin-markdown` v2, and
  279. * ESLint 7, this is the default which other JS-fenced rules will used.
  280. * Formerly "md" was the default.
  281. * @returns {{defaultFileName: string|undefined, filename: string}}
  282. */
  283. const getFilenameInfo = (filename, ext = 'md/*.js') => {
  284. let defaultFileName;
  285. if (!filename) {
  286. const jsFileName = context.getFilename();
  287. if (typeof jsFileName === 'string' && jsFileName.includes('.')) {
  288. defaultFileName = jsFileName.replace(/\.[^.]*$/u, `.${ext}`);
  289. } else {
  290. defaultFileName = `dummy.${ext}`;
  291. }
  292. }
  293. return {
  294. defaultFileName,
  295. filename
  296. };
  297. };
  298. if (checkDefaults) {
  299. const filenameInfo = getFilenameInfo(matchingFileNameDefaults, 'jsdoc-defaults');
  300. utils.forEachPreferredTag('default', (tag, targetTagName) => {
  301. if (!tag.description.trim()) {
  302. return;
  303. }
  304. checkSource({
  305. source: `(${utils.getTagDescription(tag)})`,
  306. targetTagName,
  307. ...filenameInfo
  308. });
  309. });
  310. }
  311. if (checkParams) {
  312. const filenameInfo = getFilenameInfo(matchingFileNameParams, 'jsdoc-params');
  313. utils.forEachPreferredTag('param', (tag, targetTagName) => {
  314. if (!tag.default || !tag.default.trim()) {
  315. return;
  316. }
  317. checkSource({
  318. source: `(${tag.default})`,
  319. targetTagName,
  320. ...filenameInfo
  321. });
  322. });
  323. }
  324. if (checkProperties) {
  325. const filenameInfo = getFilenameInfo(matchingFileNameProperties, 'jsdoc-properties');
  326. utils.forEachPreferredTag('property', (tag, targetTagName) => {
  327. if (!tag.default || !tag.default.trim()) {
  328. return;
  329. }
  330. checkSource({
  331. source: `(${tag.default})`,
  332. targetTagName,
  333. ...filenameInfo
  334. });
  335. });
  336. }
  337. const tagName = /** @type {string} */utils.getPreferredTagName({
  338. tagName: 'example'
  339. });
  340. if (!utils.hasTag(tagName)) {
  341. return;
  342. }
  343. const matchingFilenameInfo = getFilenameInfo(matchingFileName);
  344. utils.forEachPreferredTag('example', (tag, targetTagName) => {
  345. let source = /** @type {string} */utils.getTagDescription(tag);
  346. const match = source.match(hasCaptionRegex);
  347. if (captionRequired && (!match || !match[1].trim())) {
  348. report('Caption is expected for examples.', null, tag);
  349. }
  350. source = source.replace(hasCaptionRegex, '');
  351. const [lines, cols] = match ? getLinesCols(match[0]) : [0, 0];
  352. if (exampleCodeRegex && !exampleCodeRegex.test(source) || rejectExampleCodeRegex && rejectExampleCodeRegex.test(source)) {
  353. return;
  354. }
  355. const sources = [];
  356. let skipInit = false;
  357. if (exampleCodeRegex) {
  358. let nonJSPrefacingCols = 0;
  359. let nonJSPrefacingLines = 0;
  360. let startingIndex = 0;
  361. let lastStringCount = 0;
  362. let exampleCode;
  363. exampleCodeRegex.lastIndex = 0;
  364. while ((exampleCode = exampleCodeRegex.exec(source)) !== null) {
  365. const {
  366. index,
  367. '0': n0,
  368. '1': n1
  369. } = exampleCode;
  370. // Count anything preceding user regex match (can affect line numbering)
  371. const preMatch = source.slice(startingIndex, index);
  372. const [preMatchLines, colDelta] = getLinesCols(preMatch);
  373. let nonJSPreface;
  374. let nonJSPrefaceLineCount;
  375. if (n1) {
  376. const idx = n0.indexOf(n1);
  377. nonJSPreface = n0.slice(0, idx);
  378. nonJSPrefaceLineCount = countChars(nonJSPreface, '\n');
  379. } else {
  380. nonJSPreface = '';
  381. nonJSPrefaceLineCount = 0;
  382. }
  383. nonJSPrefacingLines += lastStringCount + preMatchLines + nonJSPrefaceLineCount;
  384. // Ignore `preMatch` delta if newlines here
  385. if (nonJSPrefaceLineCount) {
  386. const charsInLastLine = nonJSPreface.slice(nonJSPreface.lastIndexOf('\n') + 1).length;
  387. nonJSPrefacingCols += charsInLastLine;
  388. } else {
  389. nonJSPrefacingCols += colDelta + nonJSPreface.length;
  390. }
  391. const string = n1 || n0;
  392. sources.push({
  393. nonJSPrefacingCols,
  394. nonJSPrefacingLines,
  395. string
  396. });
  397. startingIndex = exampleCodeRegex.lastIndex;
  398. lastStringCount = countChars(string, '\n');
  399. if (!exampleCodeRegex.global) {
  400. break;
  401. }
  402. }
  403. skipInit = true;
  404. }
  405. checkSource({
  406. cols,
  407. lines,
  408. rules: mdRules,
  409. skipInit,
  410. source,
  411. sources,
  412. tag,
  413. targetTagName,
  414. ...matchingFilenameInfo
  415. });
  416. });
  417. }, {
  418. iterateAllJsdocs: true,
  419. meta: {
  420. docs: {
  421. description: 'Ensures that (JavaScript) examples within JSDoc adhere to ESLint rules.',
  422. url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/check-examples.md#repos-sticky-header'
  423. },
  424. schema: [{
  425. additionalProperties: false,
  426. properties: {
  427. allowInlineConfig: {
  428. default: true,
  429. type: 'boolean'
  430. },
  431. baseConfig: {
  432. type: 'object'
  433. },
  434. captionRequired: {
  435. default: false,
  436. type: 'boolean'
  437. },
  438. checkDefaults: {
  439. default: false,
  440. type: 'boolean'
  441. },
  442. checkEslintrc: {
  443. default: true,
  444. type: 'boolean'
  445. },
  446. checkParams: {
  447. default: false,
  448. type: 'boolean'
  449. },
  450. checkProperties: {
  451. default: false,
  452. type: 'boolean'
  453. },
  454. configFile: {
  455. type: 'string'
  456. },
  457. exampleCodeRegex: {
  458. type: 'string'
  459. },
  460. matchingFileName: {
  461. type: 'string'
  462. },
  463. matchingFileNameDefaults: {
  464. type: 'string'
  465. },
  466. matchingFileNameParams: {
  467. type: 'string'
  468. },
  469. matchingFileNameProperties: {
  470. type: 'string'
  471. },
  472. noDefaultExampleRules: {
  473. default: false,
  474. type: 'boolean'
  475. },
  476. paddedIndent: {
  477. default: 0,
  478. type: 'integer'
  479. },
  480. rejectExampleCodeRegex: {
  481. type: 'string'
  482. },
  483. reportUnusedDisableDirectives: {
  484. default: true,
  485. type: 'boolean'
  486. }
  487. },
  488. type: 'object'
  489. }],
  490. type: 'suggestion'
  491. }
  492. });
  493. module.exports = exports.default;
  494. //# sourceMappingURL=checkExamples.js.map