checkLineAlignment.js 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", {
  3. value: true
  4. });
  5. exports.default = void 0;
  6. var _alignTransform = _interopRequireDefault(require("../alignTransform.js"));
  7. var _iterateJsdoc = _interopRequireDefault(require("../iterateJsdoc.js"));
  8. var _commentParser = require("comment-parser");
  9. function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
  10. const {
  11. flow: commentFlow
  12. } = _commentParser.transforms;
  13. /**
  14. * @typedef {{
  15. * postDelimiter: import('../iterateJsdoc.js').Integer,
  16. * postHyphen: import('../iterateJsdoc.js').Integer,
  17. * postName: import('../iterateJsdoc.js').Integer,
  18. * postTag: import('../iterateJsdoc.js').Integer,
  19. * postType: import('../iterateJsdoc.js').Integer,
  20. * }} CustomSpacings
  21. */
  22. /**
  23. * @param {import('../iterateJsdoc.js').Utils} utils
  24. * @param {import('comment-parser').Spec & {
  25. * line: import('../iterateJsdoc.js').Integer
  26. * }} tag
  27. * @param {CustomSpacings} customSpacings
  28. */
  29. const checkNotAlignedPerTag = (utils, tag, customSpacings) => {
  30. /*
  31. start +
  32. delimiter +
  33. postDelimiter +
  34. tag +
  35. postTag +
  36. type +
  37. postType +
  38. name +
  39. postName +
  40. description +
  41. end +
  42. lineEnd
  43. */
  44. /**
  45. * @typedef {"tag"|"type"|"name"|"description"} ContentProp
  46. */
  47. /** @type {("postDelimiter"|"postTag"|"postType"|"postName")[]} */
  48. let spacerProps;
  49. /** @type {ContentProp[]} */
  50. let contentProps;
  51. const mightHaveNamepath = utils.tagMightHaveNamepath(tag.tag);
  52. if (mightHaveNamepath) {
  53. spacerProps = ['postDelimiter', 'postTag', 'postType', 'postName'];
  54. contentProps = ['tag', 'type', 'name', 'description'];
  55. } else {
  56. spacerProps = ['postDelimiter', 'postTag', 'postType'];
  57. contentProps = ['tag', 'type', 'description'];
  58. }
  59. const {
  60. tokens
  61. } = tag.source[0];
  62. /**
  63. * @param {import('../iterateJsdoc.js').Integer} idx
  64. * @param {(notRet: boolean, contentProp: ContentProp) => void} [callbck]
  65. */
  66. const followedBySpace = (idx, callbck) => {
  67. const nextIndex = idx + 1;
  68. return spacerProps.slice(nextIndex).some((spacerProp, innerIdx) => {
  69. const contentProp = contentProps[nextIndex + innerIdx];
  70. const spacePropVal = tokens[spacerProp];
  71. const ret = spacePropVal;
  72. if (callbck) {
  73. callbck(!ret, contentProp);
  74. }
  75. return ret && (callbck || !contentProp);
  76. });
  77. };
  78. const postHyphenSpacing = (customSpacings === null || customSpacings === void 0 ? void 0 : customSpacings.postHyphen) ?? 1;
  79. const exactHyphenSpacing = new RegExp(`^\\s*-\\s{${postHyphenSpacing},${postHyphenSpacing}}(?!\\s)`, 'u');
  80. const hasNoHyphen = !/^\s*-(?!$)(?=\s)/u.test(tokens.description);
  81. const hasExactHyphenSpacing = exactHyphenSpacing.test(tokens.description);
  82. // If checking alignment on multiple lines, need to check other `source`
  83. // items
  84. // Go through `post*` spacing properties and exit to indicate problem if
  85. // extra spacing detected
  86. const ok = !spacerProps.some((spacerProp, idx) => {
  87. const contentProp = contentProps[idx];
  88. const contentPropVal = tokens[contentProp];
  89. const spacerPropVal = tokens[spacerProp];
  90. const spacing = (customSpacings === null || customSpacings === void 0 ? void 0 : customSpacings[spacerProp]) || 1;
  91. // There will be extra alignment if...
  92. // 1. The spaces don't match the space it should have (1 or custom spacing) OR
  93. return spacerPropVal.length !== spacing && spacerPropVal.length !== 0 ||
  94. // 2. There is a (single) space, no immediate content, and yet another
  95. // space is found subsequently (not separated by intervening content)
  96. spacerPropVal && !contentPropVal && followedBySpace(idx);
  97. }) && (hasNoHyphen || hasExactHyphenSpacing);
  98. if (ok) {
  99. return;
  100. }
  101. const fix = () => {
  102. for (const [idx, spacerProp] of spacerProps.entries()) {
  103. const contentProp = contentProps[idx];
  104. const contentPropVal = tokens[contentProp];
  105. if (contentPropVal) {
  106. const spacing = (customSpacings === null || customSpacings === void 0 ? void 0 : customSpacings[spacerProp]) || 1;
  107. tokens[spacerProp] = ''.padStart(spacing, ' ');
  108. followedBySpace(idx, (hasSpace, contentPrp) => {
  109. if (hasSpace) {
  110. tokens[contentPrp] = '';
  111. }
  112. });
  113. } else {
  114. tokens[spacerProp] = '';
  115. }
  116. }
  117. if (!hasExactHyphenSpacing) {
  118. const hyphenSpacing = /^\s*-\s+/u;
  119. tokens.description = tokens.description.replace(hyphenSpacing, '-' + ''.padStart(postHyphenSpacing, ' '));
  120. }
  121. utils.setTag(tag, tokens);
  122. };
  123. utils.reportJSDoc('Expected JSDoc block lines to not be aligned.', tag, fix, true);
  124. };
  125. /**
  126. * @param {object} cfg
  127. * @param {CustomSpacings} cfg.customSpacings
  128. * @param {string} cfg.indent
  129. * @param {import('comment-parser').Block} cfg.jsdoc
  130. * @param {import('eslint').Rule.Node & {
  131. * range: [number, number]
  132. * }} cfg.jsdocNode
  133. * @param {boolean} cfg.preserveMainDescriptionPostDelimiter
  134. * @param {import('../iterateJsdoc.js').Report} cfg.report
  135. * @param {string[]} cfg.tags
  136. * @param {import('../iterateJsdoc.js').Utils} cfg.utils
  137. * @param {string} cfg.wrapIndent
  138. * @returns {void}
  139. */
  140. const checkAlignment = ({
  141. customSpacings,
  142. indent,
  143. jsdoc,
  144. jsdocNode,
  145. preserveMainDescriptionPostDelimiter,
  146. report,
  147. tags,
  148. utils,
  149. wrapIndent
  150. }) => {
  151. const transform = commentFlow((0, _alignTransform.default)({
  152. customSpacings,
  153. indent,
  154. preserveMainDescriptionPostDelimiter,
  155. tags,
  156. wrapIndent
  157. }));
  158. const transformedJsdoc = transform(jsdoc);
  159. const comment = '/*' +
  160. /**
  161. * @type {import('eslint').Rule.Node & {
  162. * range: [number, number], value: string
  163. * }}
  164. */
  165. jsdocNode.value + '*/';
  166. const formatted = utils.stringify(transformedJsdoc).trimStart();
  167. if (comment !== formatted) {
  168. report('Expected JSDoc block lines to be aligned.', /** @type {import('eslint').Rule.ReportFixer} */fixer => {
  169. return fixer.replaceText(jsdocNode, formatted);
  170. });
  171. }
  172. };
  173. var _default = exports.default = (0, _iterateJsdoc.default)(({
  174. indent,
  175. jsdoc,
  176. jsdocNode,
  177. report,
  178. context,
  179. utils
  180. }) => {
  181. const {
  182. tags: applicableTags = ['param', 'arg', 'argument', 'property', 'prop', 'returns', 'return'],
  183. preserveMainDescriptionPostDelimiter,
  184. customSpacings,
  185. wrapIndent = ''
  186. } = context.options[1] || {};
  187. if (context.options[0] === 'always') {
  188. // Skip if it contains only a single line.
  189. if (!(
  190. /**
  191. * @type {import('eslint').Rule.Node & {
  192. * range: [number, number], value: string
  193. * }}
  194. */
  195. jsdocNode.value.includes('\n'))) {
  196. return;
  197. }
  198. checkAlignment({
  199. customSpacings,
  200. indent,
  201. jsdoc,
  202. jsdocNode,
  203. preserveMainDescriptionPostDelimiter,
  204. report,
  205. tags: applicableTags,
  206. utils,
  207. wrapIndent
  208. });
  209. return;
  210. }
  211. const foundTags = utils.getPresentTags(applicableTags);
  212. if (context.options[0] !== 'any') {
  213. for (const tag of foundTags) {
  214. checkNotAlignedPerTag(utils,
  215. /**
  216. * @type {import('comment-parser').Spec & {
  217. * line: import('../iterateJsdoc.js').Integer
  218. * }}
  219. */
  220. tag, customSpacings);
  221. }
  222. }
  223. for (const tag of foundTags) {
  224. if (tag.source.length > 1) {
  225. let idx = 0;
  226. for (const {
  227. tokens
  228. // Avoid the tag line
  229. } of tag.source.slice(1)) {
  230. idx++;
  231. if (!tokens.description ||
  232. // Avoid first lines after multiline type
  233. tokens.type || tokens.name) {
  234. continue;
  235. }
  236. // Don't include a single separating space/tab
  237. if (tokens.postDelimiter.slice(1) !== wrapIndent) {
  238. utils.reportJSDoc('Expected wrap indent', {
  239. line: tag.source[0].number + idx
  240. }, () => {
  241. tokens.postDelimiter = tokens.postDelimiter.charAt(0) + wrapIndent;
  242. });
  243. return;
  244. }
  245. }
  246. }
  247. }
  248. }, {
  249. iterateAllJsdocs: true,
  250. meta: {
  251. docs: {
  252. description: 'Reports invalid alignment of JSDoc block lines.',
  253. url: 'https://github.com/gajus/eslint-plugin-jsdoc/blob/main/docs/rules/check-line-alignment.md#repos-sticky-header'
  254. },
  255. fixable: 'whitespace',
  256. schema: [{
  257. enum: ['always', 'never', 'any'],
  258. type: 'string'
  259. }, {
  260. additionalProperties: false,
  261. properties: {
  262. customSpacings: {
  263. additionalProperties: false,
  264. properties: {
  265. postDelimiter: {
  266. type: 'integer'
  267. },
  268. postHyphen: {
  269. type: 'integer'
  270. },
  271. postName: {
  272. type: 'integer'
  273. },
  274. postTag: {
  275. type: 'integer'
  276. },
  277. postType: {
  278. type: 'integer'
  279. }
  280. }
  281. },
  282. preserveMainDescriptionPostDelimiter: {
  283. default: false,
  284. type: 'boolean'
  285. },
  286. tags: {
  287. items: {
  288. type: 'string'
  289. },
  290. type: 'array'
  291. },
  292. wrapIndent: {
  293. type: 'string'
  294. }
  295. },
  296. type: 'object'
  297. }],
  298. type: 'layout'
  299. }
  300. });
  301. module.exports = exports.default;
  302. //# sourceMappingURL=checkLineAlignment.js.map