| import iterateJsdoc from '../iterateJsdoc.js'; |
| |
| /** |
| * @param {string} targetTagName |
| * @param {boolean} enableFixer |
| * @param {import('comment-parser').Block} jsdoc |
| * @param {import('../iterateJsdoc.js').Utils} utils |
| * @returns {boolean} |
| */ |
| const validatePropertyNames = ( |
| targetTagName, |
| enableFixer, |
| jsdoc, utils, |
| ) => { |
| const propertyTags = Object.entries(jsdoc.tags).filter(([ |
| , tag, |
| ]) => { |
| return tag.tag === targetTagName; |
| }); |
| |
| return propertyTags.some(([ |
| , tag, |
| ], index) => { |
| /** @type {import('../iterateJsdoc.js').Integer} */ |
| let tagsIndex; |
| const dupeTagInfo = propertyTags.find(([ |
| tgsIndex, |
| tg, |
| ], idx) => { |
| tagsIndex = Number(tgsIndex); |
| |
| return tg.name === tag.name && idx !== index; |
| }); |
| if (dupeTagInfo) { |
| utils.reportJSDoc(`Duplicate @${targetTagName} "${tag.name}"`, dupeTagInfo[1], enableFixer ? () => { |
| utils.removeTag(tagsIndex); |
| } : null); |
| |
| return true; |
| } |
| |
| return false; |
| }); |
| }; |
| |
| /** |
| * @param {string} targetTagName |
| * @param {{ |
| * idx: number; |
| * name: string; |
| * type: string; |
| * }[]} jsdocPropertyNames |
| * @param {import('comment-parser').Block} jsdoc |
| * @param {import('../iterateJsdoc.js').Report} report |
| */ |
| const validatePropertyNamesDeep = ( |
| targetTagName, |
| jsdocPropertyNames, jsdoc, report, |
| ) => { |
| /** @type {string} */ |
| let lastRealProperty; |
| |
| return jsdocPropertyNames.some(({ |
| idx, |
| name: jsdocPropertyName, |
| }) => { |
| const isPropertyPath = jsdocPropertyName.includes('.'); |
| |
| if (isPropertyPath) { |
| if (!lastRealProperty) { |
| report(`@${targetTagName} path declaration ("${jsdocPropertyName}") appears before any real property.`, null, jsdoc.tags[idx]); |
| |
| return true; |
| } |
| |
| let pathRootNodeName = jsdocPropertyName.slice(0, jsdocPropertyName.indexOf('.')); |
| |
| if (pathRootNodeName.endsWith('[]')) { |
| pathRootNodeName = pathRootNodeName.slice(0, -2); |
| } |
| |
| if (pathRootNodeName !== lastRealProperty) { |
| report( |
| `@${targetTagName} path declaration ("${jsdocPropertyName}") root node name ("${pathRootNodeName}") ` + |
| `does not match previous real property name ("${lastRealProperty}").`, |
| null, |
| jsdoc.tags[idx], |
| ); |
| |
| return true; |
| } |
| } else { |
| lastRealProperty = jsdocPropertyName; |
| } |
| |
| return false; |
| }); |
| }; |
| |
| export default iterateJsdoc(({ |
| context, |
| jsdoc, |
| report, |
| utils, |
| }) => { |
| const { |
| enableFixer = false, |
| } = context.options[0] || {}; |
| const jsdocPropertyNamesDeep = utils.getJsdocTagsDeep('property'); |
| if (!jsdocPropertyNamesDeep || !jsdocPropertyNamesDeep.length) { |
| return; |
| } |
| |
| const targetTagName = /** @type {string} */ (utils.getPreferredTagName({ |
| tagName: 'property', |
| })); |
| const isError = validatePropertyNames( |
| targetTagName, |
| enableFixer, |
| jsdoc, |
| utils, |
| ); |
| |
| if (isError) { |
| return; |
| } |
| |
| validatePropertyNamesDeep( |
| targetTagName, jsdocPropertyNamesDeep, jsdoc, report, |
| ); |
| }, { |
| iterateAllJsdocs: true, |
| meta: { |
| docs: { |
| description: 'Ensures that property names in JSDoc are not duplicated on the same block and that nested properties have defined roots.', |
| url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/check-property-names.md#repos-sticky-header', |
| }, |
| fixable: 'code', |
| schema: [ |
| { |
| additionalProperties: false, |
| properties: { |
| enableFixer: { |
| description: `Set to \`true\` to auto-remove \`@property\` duplicates (based on |
| identical names). |
| |
| Note that this option will remove duplicates of the same name even if |
| the definitions do not match in other ways (e.g., the second property will |
| be removed even if it has a different type or description).`, |
| type: 'boolean', |
| }, |
| }, |
| type: 'object', |
| }, |
| ], |
| type: 'suggestion', |
| }, |
| }); |