Skip to content

Commit 152ab67

Browse files
dtracersAlexej Yaroshevich
authored andcommitted
New rule: requireDescriptionCompleteSentence
The regular expression was left with only \n because if it splits at end of input then it will have an extra empty array.
1 parent a34608a commit 152ab67

File tree

3 files changed

+320
-0
lines changed

3 files changed

+320
-0
lines changed

lib/rules/validate-jsdoc/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ var validatorsByName = module.exports = {
1717
requireNewlineAfterDescription: require('./require-newline-after-description'),
1818
disallowNewlineAfterDescription: require('./disallow-newline-after-description'),
1919

20+
requireDescriptionCompleteSentence: require('./require-description-complete-sentence'),
21+
2022
checkRedundantAccess: require('./check-redundant-access'),
2123
enforceExistence: require('./enforce-existence'),
2224
leadingUnderscoreAccess: require('./leading-underscore-access')
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
module.exports = requireDescriptionCompleteSentence;
2+
module.exports.scopes = ['function', 'variable'];
3+
module.exports.options = {
4+
requireDescriptionCompleteSentence: {allowedValues: [true]}
5+
};
6+
7+
/**
8+
* Ensures that every sentence starts with an upper case letter.
9+
*
10+
* This matches when a new line or start of a blank line
11+
* does not start with an upper case letter.
12+
* It also matches a period not fllowed by an upper case letter.
13+
*/
14+
var RE_NEW_LINE_START_WITH_UPPER_CASE = /[*]*((\n[*\s]*\n)|[.])[*\s]*[a-z]/g;
15+
16+
var START_DESCRIPTION = /^[*\s]*[a-z]/g;
17+
18+
var RE_END_DESCRIPTION = /\n/g;
19+
20+
/**
21+
* Ensures next lines with uppercase letters have periods.
22+
*
23+
* This checks for the existance of a new line that starts with an
24+
* upper case letter where the previous line does not have a period
25+
* Note that numbers count as word characters.
26+
*/
27+
var RE_NEW_LINE_UPPERCASE = /\w(?![.])(\W)*\n\W*[A-Z]/g;
28+
29+
/**
30+
* Ensures that a sentence followed by a blank line has a period
31+
*
32+
* If the above line did not have a period this would match.
33+
* this also ensures that the last sentence in the description ends with a period.
34+
*/
35+
var RE_END_WITH_PERIOD = /\w(?![.])(\W)*(\n|$)[*\s]*(\n|$)/g;
36+
37+
/**
38+
* Requires description to be a complete sentence in a jsdoc comment.
39+
*
40+
* a complete sentence is defined by starting with an upper letter
41+
* and ending with a period.
42+
*
43+
* @param {(FunctionDeclaration|FunctionExpression)} node
44+
* @param {Function} err
45+
*/
46+
function requireDescriptionCompleteSentence(node, err) {
47+
var doc = node.jsdoc;
48+
if (!doc || !doc.tags.length || !doc.description || !doc.description.length) {
49+
return;
50+
}
51+
52+
var loc = doc.loc.start;
53+
var lines = doc.description.split(RE_END_DESCRIPTION);
54+
55+
var errors = [];
56+
57+
if (START_DESCRIPTION.test(doc.description)) {
58+
var matches = returnAllMatches(doc.description, START_DESCRIPTION);
59+
matches.map(function(match) {
60+
match.message = 'Description must start with an upper case letter.';
61+
match.index = match.start;
62+
});
63+
errors = errors.concat(matches);
64+
}
65+
66+
if (RE_NEW_LINE_START_WITH_UPPER_CASE.test(doc.description)) {
67+
var matches1 = returnAllMatches(doc.description, RE_NEW_LINE_START_WITH_UPPER_CASE);
68+
matches1.map(function(match) {
69+
match.message = 'Sentence must start with an upper case letter.';
70+
match.index = match.end - 1;
71+
});
72+
errors = errors.concat(matches1);
73+
}
74+
75+
if (RE_END_WITH_PERIOD.test(doc.description)) {
76+
var matches2 = returnAllMatches(doc.description, RE_END_WITH_PERIOD);
77+
matches2.map(function(match) {
78+
match.message = 'Sentence must end with a period.';
79+
match.index = match.start;
80+
});
81+
errors = errors.concat(matches2);
82+
}
83+
84+
if (RE_NEW_LINE_UPPERCASE.test(doc.description)) {
85+
var matches3 = returnAllMatches(doc.description, RE_NEW_LINE_UPPERCASE);
86+
matches3.map(function(match) {
87+
match.message = 'You started a new line with an upper case letter but ' +
88+
'previous line does not end with a period.';
89+
match.index = match.end - 1;
90+
});
91+
errors = errors.concat(matches3);
92+
}
93+
94+
computeErrors(err, loc, errors, lines);
95+
}
96+
97+
/**
98+
* Given a list of matches it records offenses.
99+
*
100+
* This will only go through the description once for all offenses.
101+
*
102+
* @param {Function} err
103+
* @param {Object} loc
104+
* @param {Array} matches An array of matching offenses.
105+
* @param {number} matches.start The starting index of the match.
106+
* @param {string} matches.message The message of the offence.
107+
* @param {Array} lines The lines in this description.
108+
*/
109+
function computeErrors(err, loc, matches, lines) {
110+
var indexInString = 0;
111+
var currentMatch = 0;
112+
for (var currentLine = 0; currentLine < lines.length &&
113+
currentMatch < matches.length; currentLine++) {
114+
115+
var nextIndexInString = indexInString + lines[currentLine].length;
116+
while (currentMatch < matches.length && matches[currentMatch].index >= indexInString &&
117+
matches[currentMatch].index <= nextIndexInString) {
118+
119+
// currentLine is to account for additional extra characters being added.
120+
var columnOffset = (matches[currentMatch].index - indexInString) - currentLine;
121+
err(matches[currentMatch].message, {
122+
line: loc.line + 1 + currentLine,
123+
column: loc.column + 3 + columnOffset
124+
});
125+
126+
currentMatch++;
127+
}
128+
indexInString = nextIndexInString;
129+
}
130+
}
131+
132+
/**
133+
* Returns all matches of regex in input as an array.
134+
*
135+
* @return {Array} Each element in the array has two values: start and end.
136+
*/
137+
function returnAllMatches(input, regex) {
138+
var match;
139+
var indexes = [];
140+
141+
// resets the last index so that exec does not return null.
142+
regex.lastIndex = 0;
143+
do {
144+
match = regex.exec(input);
145+
if (match === null) {
146+
break;
147+
}
148+
indexes.push({
149+
start: match.index,
150+
end: match.index + match[0].length
151+
});
152+
} while (match !== null);
153+
return indexes;
154+
}
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
describe('lib/rules/validate-jsdoc/require-description-complete-sentence', function () {
2+
var checker = global.checker({
3+
additionalRules: ['lib/rules/validate-jsdoc.js']
4+
});
5+
6+
describe('not configured', function() {
7+
8+
it('should report with undefined', function() {
9+
global.expect(function() {
10+
checker.configure({requireDescriptionCompleteSentence: undefined});
11+
}).to.throws(/accepted value/i);
12+
});
13+
14+
it('should report with an object', function() {
15+
global.expect(function() {
16+
checker.configure({requireDescriptionCompleteSentence: {}});
17+
}).to.throws(/accepted value/i);
18+
});
19+
20+
});
21+
22+
describe('with true', function() {
23+
checker.rules({requireDescriptionCompleteSentence: true});
24+
25+
checker.cases([
26+
/* jshint ignore:start */
27+
{
28+
it: 'should not report common cases',
29+
code: function() {
30+
function fun(p) {
31+
}
32+
33+
/**
34+
* @param p
35+
*/
36+
function fun(p) {
37+
}
38+
}
39+
}, {
40+
it: 'should report missing period at end of description',
41+
code: function () {
42+
/**
43+
* Some description
44+
* @param {number} p description without hyphen
45+
*/
46+
function fun(p) {
47+
}
48+
},
49+
errors: {
50+
line: 2,
51+
column: 18
52+
}
53+
}, {
54+
it: 'should report missing upper case letter followed by period',
55+
code: function () {
56+
/**
57+
* Some description. hola.
58+
* @param {number} p description without hyphen
59+
*/
60+
function fun(p) {
61+
}
62+
},
63+
errors: {
64+
line: 2,
65+
column: 21
66+
}
67+
}, {
68+
it: 'should report missing period at end of multi line description',
69+
code: function () {
70+
/**
71+
* Some description
72+
* that takes up multiple lines
73+
* @param {number} p description without hyphen
74+
*/
75+
function fun(p) {
76+
}
77+
},
78+
errors: {
79+
line: 3,
80+
column: 30
81+
}
82+
}, {
83+
it: 'should report missing period if upper case letter follows',
84+
code: function () {
85+
/**
86+
* Some description
87+
* That takes up multiple lines.
88+
* @param {number} p description without hyphen
89+
*/
90+
function fun(p) {
91+
}
92+
},
93+
errors: {
94+
line: 3,
95+
column: 3
96+
}
97+
}, {
98+
it: 'should report missing upper case at beginning of description',
99+
code: function () {
100+
/**
101+
* some description.
102+
* @param {number} p description without hyphen
103+
*/
104+
function fun(p) {
105+
}
106+
},
107+
errors: {
108+
line: 2,
109+
column: 3
110+
},
111+
}, {
112+
it: 'should not report missing period or missing upper case letter',
113+
code: function () {
114+
/**
115+
* Some description.
116+
*
117+
* @param {number} p description without hyphen
118+
*/
119+
function fun(p) {}
120+
}
121+
}, {
122+
it: 'should report missing period',
123+
code: function () {
124+
/**
125+
* Some description .
126+
*
127+
* @param {number} p description without hyphen
128+
*/
129+
function fun(p) {}
130+
},
131+
errors: 1
132+
}, {
133+
it: 'should report missing period at end of first line',
134+
code: function () {
135+
/**
136+
* Some description
137+
*
138+
* More description.
139+
* @param {number} p description without hyphen
140+
*/
141+
function fun(p) {
142+
}
143+
},
144+
errors: {
145+
line: 2,
146+
column: 18
147+
}
148+
}, {
149+
it: 'should not report missing period',
150+
code: function () {
151+
/**
152+
* Some description
153+
* which is continued on the next line.
154+
* @param {number} p description without hyphen
155+
*/
156+
function fun(p) {}
157+
}
158+
}
159+
/* jshint ignore:end */
160+
]);
161+
162+
});
163+
164+
});

0 commit comments

Comments
 (0)