checkParamNames.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  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. function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
  8. /**
  9. * @param {string} targetTagName
  10. * @param {boolean} allowExtraTrailingParamDocs
  11. * @param {boolean} checkDestructured
  12. * @param {boolean} checkRestProperty
  13. * @param {RegExp} checkTypesRegex
  14. * @param {boolean} disableExtraPropertyReporting
  15. * @param {boolean} enableFixer
  16. * @param {import('../jsdocUtils.js').ParamNameInfo[]} functionParameterNames
  17. * @param {import('comment-parser').Block} jsdoc
  18. * @param {import('../iterateJsdoc.js').Utils} utils
  19. * @param {import('../iterateJsdoc.js').Report} report
  20. * @returns {boolean}
  21. */
  22. const validateParameterNames = (targetTagName, allowExtraTrailingParamDocs, checkDestructured, checkRestProperty, checkTypesRegex, disableExtraPropertyReporting, enableFixer, functionParameterNames, jsdoc, utils, report) => {
  23. const paramTags = Object.entries(jsdoc.tags).filter(([, tag]) => {
  24. return tag.tag === targetTagName;
  25. });
  26. const paramTagsNonNested = paramTags.filter(([, tag]) => {
  27. return !tag.name.includes('.');
  28. });
  29. let dotted = 0;
  30. let thisOffset = 0;
  31. // eslint-disable-next-line complexity
  32. return paramTags.some(([, tag], index) => {
  33. /** @type {import('../iterateJsdoc.js').Integer} */
  34. let tagsIndex;
  35. const dupeTagInfo = paramTags.find(([tgsIndex, tg], idx) => {
  36. tagsIndex = Number(tgsIndex);
  37. return tg.name === tag.name && idx !== index;
  38. });
  39. if (dupeTagInfo) {
  40. utils.reportJSDoc(`Duplicate @${targetTagName} "${tag.name}"`, dupeTagInfo[1], enableFixer ? () => {
  41. utils.removeTag(tagsIndex);
  42. } : null);
  43. return true;
  44. }
  45. if (tag.name.includes('.')) {
  46. dotted++;
  47. return false;
  48. }
  49. let functionParameterName = functionParameterNames[index - dotted + thisOffset];
  50. if (functionParameterName === 'this' && tag.name.trim() !== 'this') {
  51. ++thisOffset;
  52. functionParameterName = functionParameterNames[index - dotted + thisOffset];
  53. }
  54. if (!functionParameterName) {
  55. if (allowExtraTrailingParamDocs) {
  56. return false;
  57. }
  58. report(`@${targetTagName} "${tag.name}" does not match an existing function parameter.`, null, tag);
  59. return true;
  60. }
  61. if (Array.isArray(functionParameterName)) {
  62. if (!checkDestructured) {
  63. return false;
  64. }
  65. if (tag.type && tag.type.search(checkTypesRegex) === -1) {
  66. return false;
  67. }
  68. const [parameterName, {
  69. names: properties,
  70. hasPropertyRest,
  71. rests,
  72. annotationParamName
  73. }] =
  74. /**
  75. * @type {[string | undefined, import('../jsdocUtils.js').FlattendRootInfo & {
  76. * annotationParamName?: string | undefined;
  77. }]} */
  78. functionParameterName;
  79. if (annotationParamName !== undefined) {
  80. const name = tag.name.trim();
  81. if (name !== annotationParamName) {
  82. report(`@${targetTagName} "${name}" does not match parameter name "${annotationParamName}"`, null, tag);
  83. }
  84. }
  85. const tagName = parameterName === undefined ? tag.name.trim() : parameterName;
  86. const expectedNames = properties.map(name => {
  87. return `${tagName}.${name}`;
  88. });
  89. const actualNames = paramTags.map(([, paramTag]) => {
  90. return paramTag.name.trim();
  91. });
  92. const actualTypes = paramTags.map(([, paramTag]) => {
  93. return paramTag.type;
  94. });
  95. const missingProperties = [];
  96. /** @type {string[]} */
  97. const notCheckingNames = [];
  98. for (const [idx, name] of expectedNames.entries()) {
  99. if (notCheckingNames.some(notCheckingName => {
  100. return name.startsWith(notCheckingName);
  101. })) {
  102. continue;
  103. }
  104. const actualNameIdx = actualNames.findIndex(actualName => {
  105. return utils.comparePaths(name)(actualName);
  106. });
  107. if (actualNameIdx === -1) {
  108. if (!checkRestProperty && rests[idx]) {
  109. continue;
  110. }
  111. const missingIndex = actualNames.findIndex(actualName => {
  112. return utils.pathDoesNotBeginWith(name, actualName);
  113. });
  114. const line = tag.source[0].number - 1 + (missingIndex > -1 ? missingIndex : actualNames.length);
  115. missingProperties.push({
  116. name,
  117. tagPlacement: {
  118. line: line === 0 ? 1 : line
  119. }
  120. });
  121. } else if (actualTypes[actualNameIdx].search(checkTypesRegex) === -1 && actualTypes[actualNameIdx] !== '') {
  122. notCheckingNames.push(name);
  123. }
  124. }
  125. const hasMissing = missingProperties.length;
  126. if (hasMissing) {
  127. for (const {
  128. tagPlacement,
  129. name: missingProperty
  130. } of missingProperties) {
  131. report(`Missing @${targetTagName} "${missingProperty}"`, null, tagPlacement);
  132. }
  133. }
  134. if (!hasPropertyRest || checkRestProperty) {
  135. /** @type {[string, import('comment-parser').Spec][]} */
  136. const extraProperties = [];
  137. for (const [idx, name] of actualNames.entries()) {
  138. const match = name.startsWith(tag.name.trim() + '.');
  139. if (match && !expectedNames.some(utils.comparePaths(name)) && !utils.comparePaths(name)(tag.name) && (!disableExtraPropertyReporting || properties.some(prop => {
  140. return prop.split('.').length >= name.split('.').length - 1;
  141. }))) {
  142. extraProperties.push([name, paramTags[idx][1]]);
  143. }
  144. }
  145. if (extraProperties.length) {
  146. for (const [extraProperty, tg] of extraProperties) {
  147. report(`@${targetTagName} "${extraProperty}" does not exist on ${tag.name}`, null, tg);
  148. }
  149. return true;
  150. }
  151. }
  152. return hasMissing;
  153. }
  154. let funcParamName;
  155. if (typeof functionParameterName === 'object') {
  156. const {
  157. name
  158. } = functionParameterName;
  159. funcParamName = name;
  160. } else {
  161. funcParamName = functionParameterName;
  162. }
  163. if (funcParamName !== tag.name.trim()) {
  164. // Todo: Improve for array or object child items
  165. const actualNames = paramTagsNonNested.map(([, {
  166. name
  167. }]) => {
  168. return name.trim();
  169. });
  170. const expectedNames = functionParameterNames.map((item, idx) => {
  171. var _item$;
  172. if (
  173. /**
  174. * @type {[string|undefined, (import('../jsdocUtils.js').FlattendRootInfo & {
  175. * annotationParamName?: string,
  176. })]} */
  177. item !== null && item !== void 0 && (_item$ = item[1]) !== null && _item$ !== void 0 && _item$.names) {
  178. return actualNames[idx];
  179. }
  180. return item;
  181. }).filter(item => {
  182. return item !== 'this';
  183. }).join(', ');
  184. report(`Expected @${targetTagName} names to be "${expectedNames}". Got "${actualNames.join(', ')}".`, null, tag);
  185. return true;
  186. }
  187. return false;
  188. });
  189. };
  190. /**
  191. * @param {string} targetTagName
  192. * @param {boolean} _allowExtraTrailingParamDocs
  193. * @param {{
  194. * name: string,
  195. * idx: import('../iterateJsdoc.js').Integer
  196. * }[]} jsdocParameterNames
  197. * @param {import('comment-parser').Block} jsdoc
  198. * @param {Function} report
  199. * @returns {boolean}
  200. */
  201. const validateParameterNamesDeep = (targetTagName, _allowExtraTrailingParamDocs, jsdocParameterNames, jsdoc, report) => {
  202. /** @type {string} */
  203. let lastRealParameter;
  204. return jsdocParameterNames.some(({
  205. name: jsdocParameterName,
  206. idx
  207. }) => {
  208. const isPropertyPath = jsdocParameterName.includes('.');
  209. if (isPropertyPath) {
  210. if (!lastRealParameter) {
  211. report(`@${targetTagName} path declaration ("${jsdocParameterName}") appears before any real parameter.`, null, jsdoc.tags[idx]);
  212. return true;
  213. }
  214. let pathRootNodeName = jsdocParameterName.slice(0, jsdocParameterName.indexOf('.'));
  215. if (pathRootNodeName.endsWith('[]')) {
  216. pathRootNodeName = pathRootNodeName.slice(0, -2);
  217. }
  218. if (pathRootNodeName !== lastRealParameter) {
  219. report(`@${targetTagName} path declaration ("${jsdocParameterName}") root node name ("${pathRootNodeName}") ` + `does not match previous real parameter name ("${lastRealParameter}").`, null, jsdoc.tags[idx]);
  220. return true;
  221. }
  222. } else {
  223. lastRealParameter = jsdocParameterName;
  224. }
  225. return false;
  226. });
  227. };
  228. var _default = exports.default = (0, _iterateJsdoc.default)(({
  229. context,
  230. jsdoc,
  231. report,
  232. utils
  233. }) => {
  234. const {
  235. allowExtraTrailingParamDocs,
  236. checkDestructured = true,
  237. checkRestProperty = false,
  238. checkTypesPattern = '/^(?:[oO]bject|[aA]rray|PlainObject|Generic(?:Object|Array))$/',
  239. enableFixer = false,
  240. useDefaultObjectProperties = false,
  241. disableExtraPropertyReporting = false
  242. } = context.options[0] || {};
  243. const checkTypesRegex = utils.getRegexFromString(checkTypesPattern);
  244. const jsdocParameterNamesDeep = utils.getJsdocTagsDeep('param');
  245. if (!jsdocParameterNamesDeep || !jsdocParameterNamesDeep.length) {
  246. return;
  247. }
  248. const functionParameterNames = utils.getFunctionParameterNames(useDefaultObjectProperties);
  249. const targetTagName = /** @type {string} */utils.getPreferredTagName({
  250. tagName: 'param'
  251. });
  252. const isError = validateParameterNames(targetTagName, allowExtraTrailingParamDocs, checkDestructured, checkRestProperty, checkTypesRegex, disableExtraPropertyReporting, enableFixer, functionParameterNames, jsdoc, utils, report);
  253. if (isError || !checkDestructured) {
  254. return;
  255. }
  256. validateParameterNamesDeep(targetTagName, allowExtraTrailingParamDocs, jsdocParameterNamesDeep, jsdoc, report);
  257. }, {
  258. meta: {
  259. docs: {
  260. description: 'Ensures that parameter names in JSDoc match those in the function declaration.',
  261. url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/check-param-names.md#repos-sticky-header'
  262. },
  263. fixable: 'code',
  264. schema: [{
  265. additionalProperties: false,
  266. properties: {
  267. allowExtraTrailingParamDocs: {
  268. type: 'boolean'
  269. },
  270. checkDestructured: {
  271. type: 'boolean'
  272. },
  273. checkRestProperty: {
  274. type: 'boolean'
  275. },
  276. checkTypesPattern: {
  277. type: 'string'
  278. },
  279. disableExtraPropertyReporting: {
  280. type: 'boolean'
  281. },
  282. enableFixer: {
  283. type: 'boolean'
  284. },
  285. useDefaultObjectProperties: {
  286. type: 'boolean'
  287. }
  288. },
  289. type: 'object'
  290. }],
  291. type: 'suggestion'
  292. }
  293. });
  294. module.exports = exports.default;
  295. //# sourceMappingURL=checkParamNames.js.map