diff --git a/README.md b/README.md index f2d1ab62c..c528fdb13 100644 --- a/README.md +++ b/README.md @@ -9684,6 +9684,11 @@ Defaults to `true`. The following patterns are considered problems: ````js +function quux (foo) { + +} +// Message: Missing JSDoc comment. + /** * @func myFunction */ @@ -12171,74 +12176,74 @@ let TestFunction: (id) => void; // Message: Missing JSDoc @param "id" declaration. /** -* A test function. -*/ + * A test function. + */ function test( -processor: (id: number) => string + processor: (id: number) => string ) { -return processor(10); + return processor(10); } // Options: [{"contexts":["TSFunctionType"]}] // Message: Missing JSDoc @param "id" declaration. /** -* A test function. -*/ + * A test function. + */ let test = (processor: (id: number) => string) => { -return processor(10); + return processor(10); } // Options: [{"contexts":["TSFunctionType"]}] // Message: Missing JSDoc @param "id" declaration. class TestClass { -/** -* A class property. -*/ -public Test: (id: number) => string; + /** + * A class property. + */ + public Test: (id: number) => string; } // Options: [{"contexts":["TSFunctionType"]}] // Message: Missing JSDoc @param "id" declaration. class TestClass { -/** -* A class method. -*/ -public TestMethod(): (id: number) => string -{ -} + /** + * A class method. + */ + public TestMethod(): (id: number) => string + { + } } // Options: [{"contexts":["TSFunctionType"]}] // Message: Missing JSDoc @param "id" declaration. interface TestInterface { /** -* An interface property. -*/ + * An interface property. + */ public Test: (id: number) => string; } // Options: [{"contexts":["TSFunctionType"]}] // Message: Missing JSDoc @param "id" declaration. interface TestInterface { -/** -* An interface method. -*/ -public TestMethod(): (id: number) => string; + /** + * An interface method. + */ + public TestMethod(): (id: number) => string; } // Options: [{"contexts":["TSFunctionType"]}] // Message: Missing JSDoc @param "id" declaration. /** -* A function with return type -*/ + * A function with return type + */ function test(): (id: number) => string; // Options: [{"contexts":["TSFunctionType"]}] // Message: Missing JSDoc @param "id" declaration. /** -* A function with return type -*/ + * A function with return type + */ let test = (): (id: number) => string => { return (id) => `${id}`; diff --git a/package.json b/package.json index 41b0d5bdf..140a17ced 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "url": "http://gajus.com" }, "dependencies": { - "comment-parser": "^0.7.6", + "comment-parser": "1.0.1", "debug": "^4.3.1", "jsdoctypeparser": "^9.0.0", "lodash": "^4.17.20", diff --git a/src/iterateJsdoc.js b/src/iterateJsdoc.js index c457f6665..64f1ebd98 100644 --- a/src/iterateJsdoc.js +++ b/src/iterateJsdoc.js @@ -1,108 +1,128 @@ import { - // eslint-disable-next-line import/no-named-default - default as commentParser, stringify as commentStringify, + parse as commentParser, stringify as commentStringify, } from 'comment-parser'; +import getSpacer from 'comment-parser/lib/parser/spacer'; +import { + tagTokenizer, + typeTokenizer, + nameTokenizer, + descriptionTokenizer, +} from 'comment-parser/lib/parser/spec-parser'; +import { + rewireSpecs, + seedBlock, + seedTokens, +} from 'comment-parser/lib/util'; import _ from 'lodash'; import { getJSDocComment, getReducedASTNode, } from './eslint/getJSDocComment'; import jsdocUtils from './jsdocUtils'; +/* +const { + align as commentAlign, + flow: commentFlow, + indent: commentIndent, +} = transforms; +*/ + const globalState = new Map(); -const skipSeeLink = (parser) => { - return (str, data) => { - if (data.tag === 'see' && (/\{@link.+?\}/u).test(str)) { - return null; - } +const hasSeeWithLink = (spec) => { + return spec.tag === 'see' && (/\{@link.+?\}/u).test(spec.source[0].source); +}; - return parser(str, data); - }; +const getTokenizers = () => { + // trim + return [ + // Tag + tagTokenizer(), + + // Type + (spec) => { + if (['default', 'defaultvalue', 'see'].includes(spec.tag)) { + return spec; + } + + return typeTokenizer()(spec); + }, + + // Name + (spec) => { + if (spec.tag === 'template') { + // const preWS = spec.postTag; + const remainder = spec.source[0].tokens.description; + + const pos = remainder.search(/(? { + return descriptionTokenizer(getSpacer('preserve'))(spec); + }, + ]; }; /** * * @param {object} commentNode * @param {string} indent Whitespace - * @param {boolean} [trim=true] * @returns {object} */ -const parseComment = (commentNode, indent, trim = true) => { +const parseComment = (commentNode, indent) => { // Preserve JSDoc block start/end indentation. - return commentParser(`${indent}/*${commentNode.value}${indent}*/`, { + return commentParser(`/*${commentNode.value}*/`, { // @see https://github.com/yavorskiy/comment-parser/issues/21 - parsers: [ - commentParser.PARSERS.parse_tag, - skipSeeLink( - (str, data) => { - if (['default', 'defaultvalue'].includes(data.tag)) { - return null; - } - - return commentParser.PARSERS.parse_type(str, data); - }, - ), - skipSeeLink( - (str, data) => { - if (data.tag === 'template') { - const preWS = str.match(/^[\t ]+/)?.[0].length ?? 0; - const remainder = str.slice(preWS); - - const pos = remainder.search(/(? { - // Only expected throw in previous step is if bad name (i.e., - // missing end bracket on optional name), but `@example` - // skips name parsing - /* istanbul ignore next */ - if (data.errors && data.errors.length) { - return null; - } - - // Tweak original regex to capture only single optional space - const result = str.match(/^ ?((.|\s)+)?/u); - - // Always has at least whitespace due to `indent` we've added - /* istanbul ignore next */ - if (result) { - return { - data: { - description: result[1] === undefined ? '' : result[1], - }, - source: result[0], - }; - } - - // Always has at least whitespace due to `indent` we've added - /* istanbul ignore next */ - return null; + tokenizers: getTokenizers(), + })[0] || seedBlock({ + source: [ + { + number: 0, + tokens: seedTokens({ + delimiter: '/**', + description: '', + end: '', + postDelimiter: '', + start: '', + }), + }, + { + number: 1, + tokens: seedTokens({ + delimiter: '', + description: '', + end: '*/', + postDelimiter: '', + start: indent + ' ', + }), }, ], - trim, - })[0] || {}; + }); }; const getBasicUtils = (context, {tagNamePreference, mode}) => { @@ -148,6 +168,7 @@ const getUtils = ( context, iteratingAll, ruleConfig, + indent, ) => { const ancestors = context.getAncestors(); const sourceCode = context.getSourceCode(); @@ -177,33 +198,98 @@ const getUtils = ( return iteratingAll && utils.hasATag(['callback', 'function', 'func', 'method']); }; - utils.stringify = (tagBlock, tag) => { - const indent = tag ? - jsdocUtils.getIndent({ - text: sourceCode.getText( - tag, - tag.loc.start.column, - ), - }) : - jsdocUtils.getIndent(sourceCode); - - if (ruleConfig.noTrim) { - const lastTag = tagBlock.tags[tagBlock.tags.length - 1]; - lastTag.description = lastTag.description.replace(/\n$/, ''); - } - - return commentStringify([tagBlock], {indent}).slice(indent.length - 1); + utils.stringify = (tagBlock, specRewire) => { + return commentStringify(specRewire ? rewireSpecs(tagBlock) : tagBlock); }; - utils.reportJSDoc = (msg, tag, handler) => { + utils.reportJSDoc = (msg, tag, handler, specRewire) => { report(msg, handler ? (fixer) => { handler(); - const replacement = utils.stringify(jsdoc, node); + const replacement = utils.stringify(jsdoc, specRewire); return fixer.replaceText(jsdocNode, replacement); } : null, tag); }; + utils.getDescription = () => { + const descriptions = []; + let lastDescriptionLine; + jsdoc.source.slice(1).some(({tokens: {description, tag, end}}, idx) => { + if (tag || end) { + lastDescriptionLine = idx; + + return true; + } + descriptions.push(description); + + return false; + }); + + return { + description: descriptions.join('\n'), + lastDescriptionLine, + }; + }; + + utils.changeTag = (tag, ...tokens) => { + tag.source.forEach((src, idx) => { + src.tokens = { + ...src.tokens, + ...tokens[idx], + }; + }); + }; + + utils.setTag = (tag) => { + tag.source = [{ + // Or tag.source[0].number? + number: tag.line, + tokens: seedTokens({ + delimiter: '*', + postDelimiter: ' ', + start: indent + ' ', + tag: '@' + tag.tag, + }), + }]; + }; + + utils.removeTag = (tagIndex) => { + const {source} = jsdoc.tags[tagIndex]; + let lastIndex; + const firstNumber = jsdoc.source[0].number; + source.forEach(({number}) => { + const sourceIndex = jsdoc.source.findIndex(({ + number: srcNumber, tokens: {end}, + }) => { + return number === srcNumber && !end; + }); + if (sourceIndex > -1) { + jsdoc.source.splice(sourceIndex, 1); + lastIndex = sourceIndex; + } + }); + jsdoc.source.slice(lastIndex).forEach((src, idx) => { + src.number = firstNumber + lastIndex + idx; + }); + }; + + utils.addTag = (targetTagName) => { + const number = (jsdoc.tags[jsdoc.tags.length - 1]?.source[0]?.number ?? 0) + 1; + jsdoc.source.splice(number, 0, { + number, + source: '', + tokens: seedTokens({ + delimiter: '*', + postDelimiter: ' ', + start: indent + ' ', + tag: `@${targetTagName}`, + }), + }); + jsdoc.source.slice(number + 1).forEach((src) => { + src.number++; + }); + }; + utils.flattenRoots = (params) => { return jsdocUtils.flattenRoots(params); }; @@ -416,9 +502,9 @@ const getUtils = ( }); if (classJsdocNode) { - const indent = ' '.repeat(classJsdocNode.loc.start.column); + const indnt = ' '.repeat(classJsdocNode.loc.start.column); - return parseComment(classJsdocNode, indent); + return parseComment(classJsdocNode, indnt); } return null; @@ -440,7 +526,7 @@ const getUtils = ( ) { return; } - const matchingJsdocTags = _.filter(jsdoc.tags || [], { + const matchingJsdocTags = _.filter(jsdoc.tags, { tag: targetTagName, }); @@ -512,6 +598,10 @@ const makeReport = (context, commentNode) => { let loc; if (jsdocLoc) { + if (!('line' in jsdocLoc)) { + jsdocLoc.line = jsdocLoc.source[0].number; + } + const lineNumber = commentNode.loc.start.line + jsdocLoc.line; loc = { @@ -562,7 +652,7 @@ const iterate = ( ) => { const sourceLine = lines[jsdocNode.loc.start.line - 1]; const indent = sourceLine.charAt(0).repeat(jsdocNode.loc.start.column); - const jsdoc = parseComment(jsdocNode, indent, !ruleConfig.noTrim); + const jsdoc = parseComment(jsdocNode, indent); const report = makeReport(context, jsdocNode); const utils = getUtils( @@ -574,6 +664,7 @@ const iterate = ( context, iteratingAll, ruleConfig, + indent, ); if ( diff --git a/src/jsdocUtils.js b/src/jsdocUtils.js index 8af76ec08..fd3317459 100644 --- a/src/jsdocUtils.js +++ b/src/jsdocUtils.js @@ -222,7 +222,7 @@ const hasParams = (functionNode) => { */ const getJsdocTagsDeep = (jsdoc : Object, targetTagName : string) : Array => { const ret = []; - (jsdoc.tags || []).forEach(({name, tag, type}, idx) => { + jsdoc.tags.forEach(({name, tag, type}, idx) => { if (tag !== targetTagName) { return; } @@ -658,7 +658,7 @@ const getContextObject = (contexts, checkJsdoc) => { return properties; }; -const filterTags = (tags = [], filter) => { +const filterTags = (tags, filter) => { return tags.filter(filter); }; @@ -690,10 +690,7 @@ const getTagsByType = (context, mode, tags, tagPreference) => { }; const getIndent = (sourceCode) => { - let indent = sourceCode.text.match(/^\n*([ \t]+)/u); - indent = indent ? indent[1] + ' ' : ' '; - - return indent; + return (sourceCode.text.match(/^\n*([ \t]+)/u)?.[1] ?? '') + ' '; }; const isConstructor = (node) => { diff --git a/src/rules/checkExamples.js b/src/rules/checkExamples.js index 520d1a1c6..33ba75558 100644 --- a/src/rules/checkExamples.js +++ b/src/rules/checkExamples.js @@ -214,6 +214,10 @@ export default iterateJsdoc(({ const {results: [{messages}]} = cliFile.executeOnText(src); + if (!('line' in tag)) { + tag.line = tag.source[0].number; + } + // NOTE: `tag.line` can be 0 if of form `/** @tag ... */` const codeStartLine = tag.line + nonJSPrefacingLines; const codeStartCol = likelyNestedJSDocIndentSpace; @@ -307,7 +311,7 @@ export default iterateJsdoc(({ const matchingFilenameInfo = getFilenameInfo(matchingFileName); utils.forEachPreferredTag('example', (tag, targetTagName) => { - let source = tag.description; + let source = tag.source[0].tokens.postTag.slice(1) + tag.description; const match = source.match(hasCaptionRegex); if (captionRequired && (!match || !match[1].trim())) { @@ -468,5 +472,4 @@ export default iterateJsdoc(({ ], type: 'suggestion', }, - noTrim: true, }); diff --git a/src/rules/checkParamNames.js b/src/rules/checkParamNames.js index fa990403c..b7763fe41 100644 --- a/src/rules/checkParamNames.js +++ b/src/rules/checkParamNames.js @@ -27,7 +27,7 @@ const validateParameterNames = ( }); if (dupeTagInfo) { utils.reportJSDoc(`Duplicate @${targetTagName} "${tag.name}"`, dupeTagInfo[1], enableFixer ? () => { - jsdoc.tags.splice(tagsIndex, 1); + utils.removeTag(tagsIndex); } : null); return true; diff --git a/src/rules/checkPropertyNames.js b/src/rules/checkPropertyNames.js index b77ded0ea..24ed66454 100644 --- a/src/rules/checkPropertyNames.js +++ b/src/rules/checkPropertyNames.js @@ -18,7 +18,7 @@ const validatePropertyNames = ( }); if (dupeTagInfo) { utils.reportJSDoc(`Duplicate @${targetTagName} "${tag.name}"`, dupeTagInfo[1], enableFixer ? () => { - jsdoc.tags.splice(tagsIndex, 1); + utils.removeTag(tagsIndex); } : null); return true; diff --git a/src/rules/checkSyntax.js b/src/rules/checkSyntax.js index f121fd58c..70864a4b8 100644 --- a/src/rules/checkSyntax.js +++ b/src/rules/checkSyntax.js @@ -5,10 +5,6 @@ export default iterateJsdoc(({ report, settings, }) => { - if (!jsdoc.tags) { - return; - } - const {mode} = settings; // Don't check for "permissive" and "closure" diff --git a/src/rules/checkTagNames.js b/src/rules/checkTagNames.js index 2d685cd3b..c5f56cf42 100644 --- a/src/rules/checkTagNames.js +++ b/src/rules/checkTagNames.js @@ -10,9 +10,6 @@ export default iterateJsdoc(({ settings, jsdocNode, }) => { - if (!jsdoc.tags) { - return; - } const {definedTags = []} = context.options[0] || {}; let definedPreferredTags = []; diff --git a/src/rules/emptyTags.js b/src/rules/emptyTags.js index 97be58178..5ca03d62f 100644 --- a/src/rules/emptyTags.js +++ b/src/rules/emptyTags.js @@ -24,9 +24,6 @@ export default iterateJsdoc(({ jsdoc, utils, }) => { - if (!jsdoc.tags) { - return; - } const emptyTags = utils.filterTags(({tag: tagName}) => { return defaultEmptyTags.has(tagName) || utils.hasOptionTag(tagName) && jsdoc.tags.some(({tag}) => { @@ -35,16 +32,12 @@ export default iterateJsdoc(({ settings.mode !== 'closure' && emptyIfNotClosure.has(tagName); }); emptyTags.forEach((tag) => { - const fix = () => { - tag.name = ''; - tag.description = ''; - tag.type = ''; - tag.optional = false; - tag.default = undefined; - }; const content = tag.name || tag.description || tag.type; if (content) { - utils.reportJSDoc(`@${tag.tag} should be empty.`, tag, fix); + const fix = () => { + utils.setTag(tag); + }; + utils.reportJSDoc(`@${tag.tag} should be empty.`, tag, fix, true); } }); }, { diff --git a/src/rules/matchDescription.js b/src/rules/matchDescription.js index 05bf18b13..c6cf8ff80 100644 --- a/src/rules/matchDescription.js +++ b/src/rules/matchDescription.js @@ -38,7 +38,7 @@ export default iterateJsdoc(({ if (!regex.test(description)) { report('JSDoc description does not satisfy the regex pattern.', null, tag || { // Add one as description would typically be into block - line: jsdoc.line + 1, + line: jsdoc.source[0].number + 1, }); } }; diff --git a/src/rules/newlineAfterDescription.js b/src/rules/newlineAfterDescription.js index 1574b34fd..38892acf2 100644 --- a/src/rules/newlineAfterDescription.js +++ b/src/rules/newlineAfterDescription.js @@ -8,6 +8,7 @@ export default iterateJsdoc(({ jsdocNode, sourceCode, indent, + utils, }) => { let always; @@ -21,13 +22,13 @@ export default iterateJsdoc(({ always = true; } - const descriptionEndsWithANewline = (/\n\r?$/).test(jsdoc.description); + const {description, lastDescriptionLine} = utils.getDescription(); + const descriptionEndsWithANewline = (/\n\r?$/).test(description); if (always) { if (!descriptionEndsWithANewline) { const sourceLines = sourceCode.getText(jsdocNode).split('\n'); - const splitDesc = jsdoc.description.split('\n'); - const lastDescriptionLine = splitDesc.length - 1; + report('There must be a newline after the description of the JSDoc block.', (fixer) => { // Add the new line const injectedLine = `${indent} *` + @@ -41,8 +42,6 @@ export default iterateJsdoc(({ } } else if (descriptionEndsWithANewline) { const sourceLines = sourceCode.getText(jsdocNode).split('\n'); - const splitDesc = jsdoc.description.split('\n'); - const lastDescriptionLine = splitDesc.length - 1; report('There must be no newline after the description of the JSDoc block.', (fixer) => { // Remove the extra line sourceLines.splice(lastDescriptionLine, 1); @@ -68,5 +67,4 @@ export default iterateJsdoc(({ ], type: 'layout', }, - noTrim: true, }); diff --git a/src/rules/noBadBlocks.js b/src/rules/noBadBlocks.js index c2a850ad2..888d18cd9 100644 --- a/src/rules/noBadBlocks.js +++ b/src/rules/noBadBlocks.js @@ -1,4 +1,6 @@ -import commentParser from 'comment-parser'; +import { + parse as commentParser, +} from 'comment-parser'; import iterateJsdoc from '../iterateJsdoc'; const commentRegexp = /^\/\*(?!\*)/; diff --git a/src/rules/noDefaults.js b/src/rules/noDefaults.js index 6f085cf6a..efaec2a9c 100644 --- a/src/rules/noDefaults.js +++ b/src/rules/noDefaults.js @@ -9,12 +9,15 @@ export default iterateJsdoc(({ paramTags.forEach((tag) => { if (noOptionalParamNames && tag.optional) { utils.reportJSDoc(`Optional param names are not permitted on @${tag.tag}.`, tag, () => { - tag.default = undefined; - tag.optional = false; + utils.changeTag(tag, { + name: tag.name.replace(/([^=]*)(=.+)?/, '$1'), + }); }); } else if (tag.default) { utils.reportJSDoc(`Defaults are not permitted on @${tag.tag}.`, tag, () => { - tag.default = undefined; + utils.changeTag(tag, { + name: tag.name.replace(/([^=]*)(=.+)?/, '[$1]'), + }); }); } }); @@ -22,7 +25,10 @@ export default iterateJsdoc(({ defaultTags.forEach((tag) => { if (tag.description) { utils.reportJSDoc(`Default values are not permitted on @${tag.tag}.`, tag, () => { - tag.description = ''; + utils.changeTag(tag, { + description: '', + postTag: '', + }); }); } }); diff --git a/src/rules/noTypes.js b/src/rules/noTypes.js index 97835cded..3d5d763f4 100644 --- a/src/rules/noTypes.js +++ b/src/rules/noTypes.js @@ -1,5 +1,10 @@ import iterateJsdoc from '../iterateJsdoc'; +const removeType = ({tokens}) => { + tokens.postTag = ''; + tokens.type = ''; +}; + export default iterateJsdoc(({ utils, }) => { @@ -12,7 +17,7 @@ export default iterateJsdoc(({ tags.forEach((tag) => { if (tag.type) { utils.reportJSDoc(`Types are not permitted on @${tag.tag}.`, tag, () => { - tag.type = ''; + tag.source.forEach(removeType); }); } }); @@ -40,5 +45,4 @@ export default iterateJsdoc(({ ], type: 'suggestion', }, - noTrim: true, }); diff --git a/src/rules/noUndefinedTypes.js b/src/rules/noUndefinedTypes.js index d504ceafc..3abd3f8a3 100644 --- a/src/rules/noUndefinedTypes.js +++ b/src/rules/noUndefinedTypes.js @@ -68,7 +68,7 @@ export default iterateJsdoc(({ return parseComment(commentNode, ''); }) .flatMap((doc) => { - return (doc.tags || []).filter(({tag}) => { + return doc.tags.filter(({tag}) => { return utils.isNamepathDefiningTag(tag); }); }) diff --git a/src/rules/requireDescriptionCompleteSentence.js b/src/rules/requireDescriptionCompleteSentence.js index d06096748..dadfad952 100644 --- a/src/rules/requireDescriptionCompleteSentence.js +++ b/src/rules/requireDescriptionCompleteSentence.js @@ -111,7 +111,11 @@ const validateDescription = ( }; const report = (msg, fixer, tagObj) => { - tagObj.line += parIdx * 2; + if ('line' in tagObj) { + tagObj.line += parIdx * 2; + } else { + tagObj.source[0].number += parIdx * 2; + } // Avoid errors if old column doesn't exist here tagObj.column = 0; @@ -126,7 +130,7 @@ const validateDescription = ( const paragraphNoAbbreviations = paragraph.replace(abbreviationsRegex, ''); - if (!/[.!?|]$/u.test(paragraphNoAbbreviations)) { + if (!/[.!?|]\s*$/u.test(paragraphNoAbbreviations)) { report('Sentence must end with a period.', fix, tag); return true; @@ -162,17 +166,17 @@ export default iterateJsdoc(({ }).join('|') + '(?:$|\\s)', 'gu') : ''; - if (!jsdoc.tags || - validateDescription(jsdoc.description, report, jsdocNode, abbreviationsRegex, sourceCode, { - line: jsdoc.line + 1, - }, newlineBeforeCapsAssumesBadSentenceEnd) - ) { + const {description} = utils.getDescription(); + + if (validateDescription(description, report, jsdocNode, abbreviationsRegex, sourceCode, { + line: jsdoc.source[0].number + 1, + }, newlineBeforeCapsAssumesBadSentenceEnd)) { return; } utils.forEachPreferredTag('description', (matchingJsdocTag) => { - const description = `${matchingJsdocTag.name} ${matchingJsdocTag.description}`.trim(); - validateDescription(description, report, jsdocNode, abbreviationsRegex, sourceCode, matchingJsdocTag, newlineBeforeCapsAssumesBadSentenceEnd); + const desc = `${matchingJsdocTag.name} ${matchingJsdocTag.description}`.trim(); + validateDescription(desc, report, jsdocNode, abbreviationsRegex, sourceCode, matchingJsdocTag, newlineBeforeCapsAssumesBadSentenceEnd); }, true); const {tagsWithNames} = utils.getTagsByType(jsdoc.tags); @@ -186,15 +190,15 @@ export default iterateJsdoc(({ }); tagsWithNames.some((tag) => { - const description = _.trimStart(tag.description, '- '); + const desc = _.trimStart(tag.description, '- '); - return validateDescription(description, report, jsdocNode, abbreviationsRegex, sourceCode, tag, newlineBeforeCapsAssumesBadSentenceEnd); + return validateDescription(desc, report, jsdocNode, abbreviationsRegex, sourceCode, tag, newlineBeforeCapsAssumesBadSentenceEnd); }); tagsWithoutNames.some((tag) => { - const description = `${tag.name} ${tag.description}`.trim(); + const desc = `${tag.name} ${tag.description}`.trim(); - return validateDescription(description, report, jsdocNode, abbreviationsRegex, sourceCode, tag, newlineBeforeCapsAssumesBadSentenceEnd); + return validateDescription(desc, report, jsdocNode, abbreviationsRegex, sourceCode, tag, newlineBeforeCapsAssumesBadSentenceEnd); }); }, { iterateAllJsdocs: true, diff --git a/src/rules/requireExample.js b/src/rules/requireExample.js index 2a576377b..5fea5b72a 100644 --- a/src/rules/requireExample.js +++ b/src/rules/requireExample.js @@ -29,18 +29,7 @@ export default iterateJsdoc(({ } utils.reportJSDoc(`Missing JSDoc @${targetTagName} declaration.`, null, () => { - if (!jsdoc.tags) { - jsdoc.tags = []; - } - const line = jsdoc.tags.length ? jsdoc.tags[jsdoc.tags.length - 1].line + 1 : 0; - jsdoc.tags.push({ - description: '', - line, - name: '', - optional: false, - tag: targetTagName, - type: '', - }); + utils.addTag(targetTagName); }); return; diff --git a/src/rules/requireHyphenBeforeParamDescription.js b/src/rules/requireHyphenBeforeParamDescription.js index 2c560f30f..722404652 100644 --- a/src/rules/requireHyphenBeforeParamDescription.js +++ b/src/rules/requireHyphenBeforeParamDescription.js @@ -54,7 +54,7 @@ export default iterateJsdoc(({ tagEntries.forEach(([tagName, circumstance]) => { if (tagName === '*') { const preferredParamTag = utils.getPreferredTagName({tagName: 'param'}); - (jsdoc.tags || []).forEach(({tag}) => { + jsdoc.tags.forEach(({tag}) => { if (tag === preferredParamTag || tagEntries.some(([tagNme]) => { return tagNme !== '*' && tagNme === tag; })) { diff --git a/src/rules/requireJsdoc.js b/src/rules/requireJsdoc.js index 2d35d4ed9..58337e9fc 100644 --- a/src/rules/requireJsdoc.js +++ b/src/rules/requireJsdoc.js @@ -227,6 +227,7 @@ export default { baseNode.loc.start.column, ), }); + const {inlineCommentBlock} = contexts.find(({context: ctxt}) => { return ctxt === node.type; }) || {}; diff --git a/src/rules/requireParam.js b/src/rules/requireParam.js index ce7268720..d71d5449f 100644 --- a/src/rules/requireParam.js +++ b/src/rules/requireParam.js @@ -96,15 +96,22 @@ export default iterateJsdoc(({ }); }); - if (foundIndex > -1) { - return foundIndex; - } + const tags = foundIndex > -1 ? + jsdocTags.slice(0, foundIndex) : + jsdocTags.filter(({tag}) => { + return tag === preferredTagName; + }); - const paramTags = jsdocTags.filter(({tag}) => { - return tag === preferredTagName; + let tagLineCount = 0; + tags.forEach(({source}) => { + source.forEach(({tokens: {end}}) => { + if (!end) { + tagLineCount++; + } + }); }); - return paramTags.length; + return tagLineCount; }; let [nextRootName, incremented, namer] = rootNamer([...unnamedRootBase], autoIncrementBase); @@ -202,7 +209,7 @@ export default iterateJsdoc(({ ), functionParameterName: fullParamName, inc, - type: hasRestElement && !hasPropertyRest ? '...any' : undefined, + type: hasRestElement && !hasPropertyRest ? '{...any}' : undefined, }); } }); @@ -216,7 +223,7 @@ export default iterateJsdoc(({ return; } funcParamName = functionParameterName.name; - type = '...any'; + type = '{...any}'; } else { funcParamName = functionParameterName; } @@ -235,35 +242,56 @@ export default iterateJsdoc(({ const fix = ({ functionParameterIdx, functionParameterName, remove, inc, type, - }, tags) => { + }) => { if (inc && !enableRootFixer) { return; } - if (remove) { - tags.splice(functionParameterIdx, 1, { + const createTokens = (tagIndex, sourceIndex, spliceCount) => { + // console.log(sourceIndex, tagIndex, jsdoc.tags, jsdoc.source); + const tokens = { + number: sourceIndex + 1, + tokens: { + delimiter: '*', + description: '', + end: '', + name: functionParameterName, + newAdd: true, + postDelimiter: ' ', + postName: '', + postTag: ' ', + postType: type ? ' ' : '', + start: jsdoc.source[sourceIndex].tokens.start, + tag: `@${preferredTagName}`, + type: type ?? '', + }, + }; + jsdoc.tags.splice(tagIndex, spliceCount, { name: functionParameterName, newAdd: true, + source: [tokens], tag: preferredTagName, - type, + type: type ?? '', }); - } else { - const expectedIdx = findExpectedIndex(tags, functionParameterIdx); - tags.splice(expectedIdx, 0, { - name: functionParameterName, - newAdd: true, - tag: preferredTagName, - type, + const firstNumber = jsdoc.source[0].number; + jsdoc.source.splice(sourceIndex, spliceCount, tokens); + jsdoc.source.slice(sourceIndex).forEach((src, idx) => { + src.number = firstNumber + sourceIndex + idx; }); + }; + const offset = jsdoc.source.findIndex(({tokens: {tag, end}}) => { + return tag || end; + }); + if (remove) { + createTokens(functionParameterIdx, offset + functionParameterIdx, 1); + } else { + const expectedIdx = findExpectedIndex(jsdoc.tags, functionParameterIdx); + createTokens(expectedIdx, offset + expectedIdx, 0); } }; const fixer = () => { - if (!jsdoc.tags) { - jsdoc.tags = []; - } - missingTags.forEach((missingTag) => { - fix(missingTag, jsdoc.tags); + fix(missingTag); }); }; diff --git a/src/rules/requireProperty.js b/src/rules/requireProperty.js index b2a6eb7c0..a0e749cfc 100644 --- a/src/rules/requireProperty.js +++ b/src/rules/requireProperty.js @@ -1,7 +1,6 @@ import iterateJsdoc from '../iterateJsdoc'; export default iterateJsdoc(({ - jsdoc, utils, }) => { const propertyAssociatedTags = utils.filterTags(({tag}) => { @@ -21,15 +20,7 @@ export default iterateJsdoc(({ return; } utils.reportJSDoc(`Missing JSDoc @${targetTagName}.`, null, () => { - const line = jsdoc.tags[jsdoc.tags.length - 1].line + 1; - jsdoc.tags.push({ - description: '', - line, - name: '', - optional: false, - tag: targetTagName, - type: '', - }); + utils.addTag(targetTagName); }); }); }, { diff --git a/src/rules/validTypes.js b/src/rules/validTypes.js index db25f94d7..6fe8fe2c6 100644 --- a/src/rules/validTypes.js +++ b/src/rules/validTypes.js @@ -16,9 +16,6 @@ export default iterateJsdoc(({ allowEmptyNamepaths = false, } = context.options[0] || {}; const {mode} = settings; - if (!jsdoc.tags) { - return; - } const tryParseIgnoreError = (path) => { try { diff --git a/test/iterateJsdoc.js b/test/iterateJsdoc.js index d402a05d0..8ccfd0410 100644 --- a/test/iterateJsdoc.js +++ b/test/iterateJsdoc.js @@ -61,19 +61,76 @@ describe('iterateJsdoc', () => { describe('parseComment', () => { context('Parses comments', () => { it('', () => { + const tagSource = [ + { + number: 1, + source: ' @param {MyType} name desc', + tokens: { + delimiter: '', + description: 'desc', + end: '', + name: 'name', + postDelimiter: '', + postName: ' ', + postTag: ' ', + postType: ' ', + start: ' ', + tag: '@param', + type: '{MyType}', + }, + }, + { + number: 2, + source: ' */', + tokens: { + delimiter: '', + description: '', + end: '*/', + name: '', + postDelimiter: '', + postName: '', + postTag: '', + postType: '', + start: ' ', + tag: '', + type: '', + }, + }, + ]; expect(parseComment({value: `* SomeDescription @param {MyType} name desc `}, '')).to.deep.equal({ description: 'SomeDescription', - line: 0, - source: 'SomeDescription\n@param {MyType} name desc', + problems: [], + source: [ + { + number: 0, + source: '/** SomeDescription', + tokens: { + delimiter: '/**', + description: 'SomeDescription', + end: '', + name: '', + postDelimiter: ' ', + postName: '', + postTag: '', + postType: '', + start: '', + tag: '', + type: '', + }, + }, + ...tagSource, + ], tags: [ { - description: 'desc', - line: 1, + description: ' desc', name: 'name', optional: false, - source: '@param {MyType} name desc', + problems: [], + source: [ + ...tagSource, + ], tag: 'param', type: 'MyType', }, diff --git a/test/rules/assertions/requireDescriptionCompleteSentence.js b/test/rules/assertions/requireDescriptionCompleteSentence.js index 7fd892c2b..a3bae5eb7 100644 --- a/test/rules/assertions/requireDescriptionCompleteSentence.js +++ b/test/rules/assertions/requireDescriptionCompleteSentence.js @@ -691,6 +691,14 @@ export default { options: [{ abbreviations: ['Mr'], }], + output: ` + /** + * Sorry, but this isn't a complete sentence Mr. . + */ + function quux () { + + } + `, }, { code: ` diff --git a/test/rules/assertions/requireHyphenBeforeParamDescription.js b/test/rules/assertions/requireHyphenBeforeParamDescription.js index 2efc93fc4..e3af5fe50 100644 --- a/test/rules/assertions/requireHyphenBeforeParamDescription.js +++ b/test/rules/assertions/requireHyphenBeforeParamDescription.js @@ -165,7 +165,7 @@ export default { ], output: ` /** - * @param foo Foo. + * @param foo Foo. */ function quux () { diff --git a/test/rules/assertions/requireJsdoc.js b/test/rules/assertions/requireJsdoc.js index 01fe6de3f..361830fb7 100644 --- a/test/rules/assertions/requireJsdoc.js +++ b/test/rules/assertions/requireJsdoc.js @@ -6,6 +6,26 @@ export default { invalid: [ { code: ` +function quux (foo) { + +}`, + errors: [ + { + endLine: undefined, + line: 2, + message: 'Missing JSDoc comment.', + }, + ], + output: ` +/** + * + */ +function quux (foo) { + +}`, + }, + { + code: ` /** * @func myFunction */ diff --git a/test/rules/assertions/requireParam.js b/test/rules/assertions/requireParam.js index 0865fa020..5a7532511 100644 --- a/test/rules/assertions/requireParam.js +++ b/test/rules/assertions/requireParam.js @@ -1309,12 +1309,12 @@ export default { { code: ` /** - * A test function. - */ + * A test function. + */ function test( - processor: (id: number) => string + processor: (id: number) => string ) { - return processor(10); + return processor(10); } `, errors: [ @@ -1334,9 +1334,9 @@ export default { * @param id */ function test( - processor: (id: number) => string + processor: (id: number) => string ) { - return processor(10); + return processor(10); } `, parser: require.resolve('@typescript-eslint/parser'), @@ -1344,11 +1344,11 @@ export default { { code: ` /** - * A test function. - */ + * A test function. + */ let test = (processor: (id: number) => string) => { - return processor(10); + return processor(10); } `, errors: [ @@ -1369,7 +1369,7 @@ export default { */ let test = (processor: (id: number) => string) => { - return processor(10); + return processor(10); } `, parser: require.resolve('@typescript-eslint/parser'), @@ -1377,10 +1377,10 @@ export default { { code: ` class TestClass { - /** - * A class property. - */ - public Test: (id: number) => string; + /** + * A class property. + */ + public Test: (id: number) => string; } `, errors: [ @@ -1396,11 +1396,11 @@ export default { ], output: ` class TestClass { - /** - * A class property. - * @param id - */ - public Test: (id: number) => string; + /** + * A class property. + * @param id + */ + public Test: (id: number) => string; } `, parser: require.resolve('@typescript-eslint/parser'), @@ -1408,12 +1408,12 @@ export default { { code: ` class TestClass { - /** - * A class method. - */ - public TestMethod(): (id: number) => string - { - } + /** + * A class method. + */ + public TestMethod(): (id: number) => string + { + } } `, errors: [ @@ -1429,13 +1429,13 @@ export default { ], output: ` class TestClass { - /** - * A class method. - * @param id - */ - public TestMethod(): (id: number) => string - { - } + /** + * A class method. + * @param id + */ + public TestMethod(): (id: number) => string + { + } } `, parser: require.resolve('@typescript-eslint/parser'), @@ -1444,8 +1444,8 @@ export default { code: ` interface TestInterface { /** - * An interface property. - */ + * An interface property. + */ public Test: (id: number) => string; } `, @@ -1474,10 +1474,10 @@ export default { { code: ` interface TestInterface { - /** - * An interface method. - */ - public TestMethod(): (id: number) => string; + /** + * An interface method. + */ + public TestMethod(): (id: number) => string; } `, errors: [ @@ -1493,11 +1493,11 @@ export default { ], output: ` interface TestInterface { - /** - * An interface method. - * @param id - */ - public TestMethod(): (id: number) => string; + /** + * An interface method. + * @param id + */ + public TestMethod(): (id: number) => string; } `, parser: require.resolve('@typescript-eslint/parser'), @@ -1505,8 +1505,8 @@ export default { { code: ` /** - * A function with return type - */ + * A function with return type + */ function test(): (id: number) => string; `, errors: [ @@ -1532,8 +1532,8 @@ export default { { code: ` /** - * A function with return type - */ + * A function with return type + */ let test = (): (id: number) => string => { return (id) => \`\${id}\`; @@ -1626,15 +1626,15 @@ export default { class Client { /** * Set collection data. - * @param {Object} data The collection data object. - * @param {number} data.last_modified - * @param {Object} options The options object. - * @param {Object} [options.headers] The headers object option. - * @param {Number} [options.retry=0] Number of retries to make - * when faced with transient errors. - * @param {Boolean} [options.safe] The safe option. - * @param {Boolean} [options.patch] The patch option. - * @param {Number} [options.last_modified] The last_modified option. + * @param {Object} data The collection data object. + * @param {number} data.last_modified + * @param {Object} options The options object. + * @param {Object} [options.headers] The headers object option. + * @param {Number} [options.retry=0] Number of retries to make + * when faced with transient errors. + * @param {Boolean} [options.safe] The safe option. + * @param {Boolean} [options.patch] The patch option. + * @param {Number} [options.last_modified] The last_modified option. * @param options.permissions * @return {Promise} */