Skip to content

Commit

Permalink
feat(require-description-complete-sentence): add abbreviations op…
Browse files Browse the repository at this point in the history
…tion; fixes #424
  • Loading branch information
brettz9 committed Dec 31, 2019
1 parent 3c78408 commit f70fd6c
Show file tree
Hide file tree
Showing 4 changed files with 500 additions and 13 deletions.
11 changes: 10 additions & 1 deletion .README/rules/require-description-complete-sentence.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ tag descriptions are written in complete sentences, i.e.,
character must be preceded by a line ending with a period.
* A colon or semi-colon followed by two line breaks is still part of the
containing paragraph (unlike normal dual line breaks).
* Text within inline tags `{...}` are not checked for sentence divisions.
* Periods after items within the `abbreviations` option array are not treated
as sentence endings.

#### Options

Expand All @@ -34,10 +37,16 @@ its "description" (e.g., for `@returns {someType} some description`, the
description is `some description` while for `@some-tag xyz`, the description
is `xyz`).

##### `abbreviations`

You can provide an `abbreviations` options array to avoid such strings of text
being treated as sentence endings when followed by dots. The `.` is not
necessary at the end of the array items.

|||
|---|---|
|Context|everywhere|
|Tags|doc block, `param`, `returns`, `description`, `property`, `summary`, `file`, `classdesc`, `todo`, `deprecated`, `throws`, 'yields' and others added by `tags`|
|Aliases|`arg`, `argument`, `return`, `desc`, `prop`, `fileoverview`, `overview`, `exception`, `yield`|
|Options|`tags`|
|Options|`tags`, `abbreviations`|
<!-- assertions requireDescriptionCompleteSentence -->
171 changes: 170 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5226,6 +5226,9 @@ tag descriptions are written in complete sentences, i.e.,
character must be preceded by a line ending with a period.
* A colon or semi-colon followed by two line breaks is still part of the
containing paragraph (unlike normal dual line breaks).
* Text within inline tags `{...}` are not checked for sentence divisions.
* Periods after items within the `abbreviations` option array are not treated
as sentence endings.
<a name="eslint-plugin-jsdoc-rules-require-description-complete-sentence-options-11"></a>
#### Options
Expand All @@ -5252,12 +5255,19 @@ its "description" (e.g., for `@returns {someType} some description`, the
description is `some description` while for `@some-tag xyz`, the description
is `xyz`).
<a name="eslint-plugin-jsdoc-rules-require-description-complete-sentence-options-11-abbreviations"></a>
##### <code>abbreviations</code>
You can provide an `abbreviations` options array to avoid such strings of text
being treated as sentence endings when followed by dots. The `.` is not
necessary at the end of the array items.
|||
|---|---|
|Context|everywhere|
|Tags|doc block, `param`, `returns`, `description`, `property`, `summary`, `file`, `classdesc`, `todo`, `deprecated`, `throws`, 'yields' and others added by `tags`|
|Aliases|`arg`, `argument`, `return`, `desc`, `prop`, `fileoverview`, `overview`, `exception`, `yield`|
|Options|`tags`|
|Options|`tags`, `abbreviations`|
The following patterns are considered problems:
````js
Expand Down Expand Up @@ -5471,6 +5481,86 @@ function quux (foo) {
// Settings: {"jsdoc":{"tagNamePreference":{"description":false}}}
// Options: [{"tags":["param"]}]
// Message: Sentence must end with a period.
/**
* Sorry, but this isn't a complete sentence, Mr.
*/
function quux () {
}
// Options: [{"abbreviations":["Mr"]}]
// Message: Sentence must end with a period.
/**
* Sorry, but this isn't a complete sentence Mr.
*/
function quux () {
}
// Options: [{"abbreviations":["Mr."]}]
// Message: Sentence must end with a period.
/**
* Sorry, but this isn't a complete sentence Mr.
*/
function quux () {
}
// Options: [{"abbreviations":["Mr"]}]
// Message: Sentence must end with a period.
/**
* Sorry, but this isn't a complete sentence Mr. and Mrs.
*/
function quux () {
}
// Options: [{"abbreviations":["Mr","Mrs"]}]
// Message: Sentence must end with a period.
/**
* This is a complete sentence. But this isn't, Mr.
*/
function quux () {
}
// Options: [{"abbreviations":["Mr"]}]
// Message: Sentence must end with a period.
/**
* This is a complete Mr. sentence. But this isn't, Mr.
*/
function quux () {
}
// Options: [{"abbreviations":["Mr"]}]
// Message: Sentence must end with a period.
/**
* This is a complete Mr. sentence.
*/
function quux () {
}
// Message: Sentence should start with an uppercase character.
/**
* This is fun, i.e. enjoyable, but not superlatively so, e.g. not
* super, wonderful, etc..
*/
function quux () {
}
// Message: Sentence should start with an uppercase character.
/**
* Do not have dynamic content; e.g. homepage. Here a simple unique id
* suffices.
*/
function quux () {
}
// Message: Sentence should start with an uppercase character.
````
The following patterns are not considered problems:
Expand Down Expand Up @@ -5704,6 +5794,85 @@ function quux () {
function quux () {
}
/**
* Sorry, but this isn't a complete sentence, Mr.
*/
function quux () {
}
/**
* Sorry, but this isn't a complete sentence Mr..
*/
function quux () {
}
// Options: [{"abbreviations":["Mr."]}]
/**
* Sorry, but this isn't a complete sentence Mr.
*/
function quux () {
}
/**
* Sorry, but this isn't a complete sentence Mr. and Mrs..
*/
function quux () {
}
// Options: [{"abbreviations":["Mr","Mrs"]}]
/**
* This is a complete sentence aMr.
*/
function quux () {
}
// Options: [{"abbreviations":["Mr"]}]
/**
* This is a complete sentence. But this isn't, Mr.
*/
function quux () {
}
/**
* This is a complete Mr. Sentence. But this isn't, Mr.
*/
function quux () {
}
/**
* This is a complete Mr. sentence.
*/
function quux () {
}
// Options: [{"abbreviations":["Mr"]}]
/**
* This is fun, i.e. enjoyable, but not superlatively so, e.g. not
* super, wonderful, etc..
*/
function quux () {
}
// Options: [{"abbreviations":["etc","e.g.","i.e."]}]
**
* Do not have dynamic content; e.g. homepage. Here a simple unique id
* suffices.
*/
function quux () {
}
// Options: [{"abbreviations":["etc","e.g.","i.e."]}]
````
Expand Down
46 changes: 35 additions & 11 deletions src/rules/requireDescriptionCompleteSentence.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,14 @@ const extractParagraphs = (text) => {
}).reverse();
};

const extractSentences = (text) => {
const extractSentences = (text, abbreviationsRegex) => {
const txt = text

// Remove all {} tags.
.replace(/\{[\s\S]*?\}\s*/gu, '');
.replace(/\{[\s\S]*?\}\s*/gu, '')

// Remove custom abbreviations
.replace(abbreviationsRegex, '');

const sentenceEndGrouping = /([.?!])(?:\s+|$)/u;
const puncts = RegExtras(sentenceEndGrouping).map(txt, (punct) => {
Expand Down Expand Up @@ -67,15 +70,15 @@ const capitalize = (str) => {
return str.charAt(0).toUpperCase() + str.slice(1);
};

const validateDescription = (description, reportOrig, jsdocNode, sourceCode, tag) => {
const validateDescription = (description, reportOrig, jsdocNode, abbreviationsRegex, sourceCode, tag) => {
if (!description) {
return false;
}

const paragraphs = extractParagraphs(description);

return paragraphs.some((paragraph, parIdx) => {
const sentences = extractSentences(paragraph);
const sentences = extractSentences(paragraph, abbreviationsRegex);

const fix = (fixer) => {
let text = sourceCode.getText(jsdocNode);
Expand All @@ -87,7 +90,8 @@ const validateDescription = (description, reportOrig, jsdocNode, sourceCode, tag
}

for (const sentence of sentences.filter((sentence_) => {
return !(/^\s*$/u).test(sentence_) && !isCapitalized(sentence_) && !isTable(sentence_);
return !(/^\s*$/u).test(sentence_) && !isCapitalized(sentence_) &&
!isTable(sentence_);
})) {
const beginning = sentence.split('\n')[0];

Expand Down Expand Up @@ -119,13 +123,15 @@ const validateDescription = (description, reportOrig, jsdocNode, sourceCode, tag
report('Sentence should start with an uppercase character.', fix, tag);
}

if (!/[.!?|]$/u.test(paragraph)) {
const paragraphNoAbbreviations = paragraph.replace(abbreviationsRegex, '');

if (!/[.!?|]$/u.test(paragraphNoAbbreviations)) {
report('Sentence must end with a period.', fix, tag);

return true;
}

if (!isNewLinePrecededByAPeriod(paragraph)) {
if (!isNewLinePrecededByAPeriod(paragraphNoAbbreviations)) {
report('A line of text is started with an uppercase character, but preceding line does not end the sentence.', null, tag);

return true;
Expand All @@ -137,13 +143,25 @@ const validateDescription = (description, reportOrig, jsdocNode, sourceCode, tag

export default iterateJsdoc(({
sourceCode,
context,
jsdoc,
report,
jsdocNode,
utils,
}) => {
const options = context.options[0] || {};
const {
abbreviations = [],
} = options;

const abbreviationsRegex = abbreviations.length ?
new RegExp('\\b' + abbreviations.map((abbreviation) => {
return _.escapeRegExp(abbreviation.replace(/\.$/g, '') + '.');
}).join('|') + '(?:$|\\s)', 'gu') :
'';

if (!jsdoc.tags ||
validateDescription(jsdoc.description, report, jsdocNode, sourceCode, {
validateDescription(jsdoc.description, report, jsdocNode, abbreviationsRegex, sourceCode, {
line: jsdoc.line + 1,
})
) {
Expand All @@ -152,7 +170,7 @@ export default iterateJsdoc(({

utils.forEachPreferredTag('description', (matchingJsdocTag) => {
const description = `${matchingJsdocTag.name} ${matchingJsdocTag.description}`.trim();
validateDescription(description, report, jsdocNode, sourceCode, matchingJsdocTag);
validateDescription(description, report, jsdocNode, abbreviationsRegex, sourceCode, matchingJsdocTag);
}, true);

const {tagsWithNames} = utils.getTagsByType(jsdoc.tags);
Expand All @@ -168,13 +186,13 @@ export default iterateJsdoc(({
tagsWithNames.some((tag) => {
const description = _.trimStart(tag.description, '- ');

return validateDescription(description, report, jsdocNode, sourceCode, tag);
return validateDescription(description, report, jsdocNode, abbreviationsRegex, sourceCode, tag);
});

tagsWithoutNames.some((tag) => {
const description = `${tag.name} ${tag.description}`.trim();

return validateDescription(description, report, jsdocNode, sourceCode, tag);
return validateDescription(description, report, jsdocNode, abbreviationsRegex, sourceCode, tag);
});
}, {
iterateAllJsdocs: true,
Expand All @@ -184,6 +202,12 @@ export default iterateJsdoc(({
{
additionalProperties: false,
properties: {
abbreviations: {
items: {
type: 'string',
},
type: 'array',
},
tags: {
items: {
type: 'string',
Expand Down

0 comments on commit f70fd6c

Please sign in to comment.