11module . exports = requireDescriptionCompleteSentence ;
2- module . exports . scopes = [ 'function' ] ;
2+ module . exports . scopes = [ 'function' , 'variable' ] ;
33module . exports . options = {
44 requireDescriptionCompleteSentence : { allowedValues : [ true ] }
55} ;
66
7- var RE_START_WITH_UPPER_CASE = / ^ [ A - Z ] / ;
8-
97/**
10- * Checks that a period is next to a word at the end of input .
8+ * Ensures that every sentence starts with an upper case letter .
119 *
12- * The sentence ending with a new line is also valid.
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.
1313 */
14- var RE_END_WITH_PERIOD = / \w [ . ] ( \n | $ ) / ;
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;
1517
1618var RE_END_DESCRIPTION = / \n / g;
1719
18- var RE_LAST_WORD = / \w (? ! .* \w ) / ;
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;
1936
2037/**
2138 * Requires description to be a complete sentence in a jsdoc comment.
2239 *
23- * A complete sentence is defined by starting with an upper letter
40+ * a complete sentence is defined by starting with an upper letter
2441 * and ending with a period.
2542 *
2643 * @param {(FunctionDeclaration|FunctionExpression) } node
@@ -33,30 +50,105 @@ function requireDescriptionCompleteSentence(node, err) {
3350 }
3451
3552 var loc = doc . loc . start ;
53+ var lines = doc . description . split ( RE_END_DESCRIPTION ) ;
54+
55+ var errors = [ ] ;
3656
37- if ( ! RE_START_WITH_UPPER_CASE . test ( doc . description ) ) {
38- err ( 'Sentence must start with an upper case letter.' , {
39- line : loc . line + 1 ,
40- column : loc . column + 3
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 ;
4162 } ) ;
63+ errors = errors . concat ( matches ) ;
4264 }
4365
44- if ( ! RE_END_WITH_PERIOD . test ( doc . description ) ) {
45- var lines = doc . description . split ( RE_END_DESCRIPTION ) ;
46-
47- // Find the location last word in the description.
48- var line = - 1 ;
49- var column = - 1 ;
50- for ( var i = lines . length - 1 ; i >= 0 ; i -- ) {
51- if ( lines [ i ] && lines [ i ] . search ( RE_LAST_WORD ) >= 0 ) {
52- line = i ;
53- column = lines [ i ] . search ( RE_LAST_WORD ) ;
54- break ;
55- }
56- }
57- err ( 'Sentence must end with a period.' , {
58- line : loc . line + 1 + line ,
59- column : loc . column + 3 + column
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 ;
6071 } ) ;
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 ;
61129 }
62130}
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+ }
0 commit comments