Skip to content
This repository was archived by the owner on Dec 4, 2018. It is now read-only.

Commit 827f314

Browse files
Update: support node ranges (fixes #89) (#190)
This adds a `range` option to the `parse` API. When enabled, each node has a [start, end] range property indicating its location in the comment. When computing the range with the `unwrap: true` option, the returned range needs to track indices in the original comment, not the unwrapped version. To implement that behavior, this commit updates the unwrapping logic to use a regular expression rather than a state machine. When converting an index, the parser and re-matches the original comment line-by-line, keeping track of the number of discarded "wrapping" characters.
1 parent c0a1459 commit 827f314

File tree

4 files changed

+556
-335
lines changed

4 files changed

+556
-335
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ The primary method is `parse()`, which accepts two arguments: the JSDoc comment
3535
* `recoverable` - set to `true` to keep parsing even when syntax errors occur. Default: `false`.
3636
* `sloppy` - set to `true` to allow optional parameters to be specified in brackets (`@param {string} [foo]`). Default: `false`.
3737
* `lineNumbers` - set to `true` to add `lineNumber` to each node, specifying the line on which the node is found in the source. Default: `false`.
38+
* `range` - set to `true` to add `range` to each node, specifying the start and end index of the node in the original comment. Default: `false`.
3839

3940
Here's a simple example:
4041

lib/doctrine.js

Lines changed: 57 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -90,59 +90,49 @@
9090
title === 'public' || title === 'private' || title === 'protected';
9191
}
9292

93+
// A regex character class that contains all whitespace except linebreak characters (\r, \n, \u2028, \u2029)
94+
var WHITESPACE = '[ \\f\\t\\v\\u00a0\\u1680\\u180e\\u2000-\\u200a\\u202f\\u205f\\u3000\\ufeff]';
95+
96+
var STAR_MATCHER = '(' + WHITESPACE + '*(?:\\*' + WHITESPACE + '?)?)(.+|[\r\n\u2028\u2029])';
97+
9398
function unwrapComment(doc) {
9499
// JSDoc comment is following form
95100
// /**
96101
// * .......
97102
// */
98-
// remove /**, */ and *
99-
var BEFORE_STAR = 0,
100-
STAR = 1,
101-
AFTER_STAR = 2,
102-
index,
103-
len,
104-
mode,
105-
result,
106-
ch;
107-
108-
doc = doc.replace(/^\/\*\*?/, '').replace(/\*\/$/, '');
109-
index = 0;
110-
len = doc.length;
111-
mode = BEFORE_STAR;
112-
result = '';
113-
114-
while (index < len) {
115-
ch = doc.charCodeAt(index);
116-
switch (mode) {
117-
case BEFORE_STAR:
118-
if (esutils.code.isLineTerminator(ch)) {
119-
result += String.fromCharCode(ch);
120-
} else if (ch === 0x2A /* '*' */) {
121-
mode = STAR;
122-
} else if (!esutils.code.isWhiteSpace(ch)) {
123-
result += String.fromCharCode(ch);
124-
mode = AFTER_STAR;
125-
}
126-
break;
127103

128-
case STAR:
129-
if (!esutils.code.isWhiteSpace(ch)) {
130-
result += String.fromCharCode(ch);
131-
}
132-
mode = esutils.code.isLineTerminator(ch) ? BEFORE_STAR : AFTER_STAR;
133-
break;
104+
return doc.
105+
// remove /**
106+
replace(/^\/\*\*?/, '').
107+
// remove */
108+
replace(/\*\/$/, '').
109+
// remove ' * ' at the beginning of a line
110+
replace(new RegExp(STAR_MATCHER, 'g'), '$2').
111+
// remove trailing whitespace
112+
replace(/\s*$/, '');
113+
}
134114

135-
case AFTER_STAR:
136-
result += String.fromCharCode(ch);
137-
if (esutils.code.isLineTerminator(ch)) {
138-
mode = BEFORE_STAR;
139-
}
140-
break;
115+
/**
116+
* Converts an index in an "unwrapped" JSDoc comment to the corresponding index in the original "wrapped" version
117+
* @param {string} originalSource The original wrapped comment
118+
* @param {number} unwrappedIndex The index of a character in the unwrapped string
119+
* @returns {number} The index of the corresponding character in the original wrapped string
120+
*/
121+
function convertUnwrappedCommentIndex(originalSource, unwrappedIndex) {
122+
var replacedSource = originalSource.replace(/^\/\*\*?/, '');
123+
var numSkippedChars = 0;
124+
var matcher = new RegExp(STAR_MATCHER, 'g');
125+
var match;
126+
127+
while ((match = matcher.exec(replacedSource))) {
128+
numSkippedChars += match[1].length;
129+
130+
if (match.index + match[0].length > unwrappedIndex + numSkippedChars) {
131+
return unwrappedIndex + numSkippedChars + originalSource.length - replacedSource.length;
141132
}
142-
index += 1;
143133
}
144134

145-
return result.replace(/\s+$/, '');
135+
return originalSource.replace(/\*\/$/, '').replace(/\s*$/, '').length;
146136
}
147137

148138
// JSDoc Tag Parser
@@ -153,6 +143,7 @@
153143
lineNumber,
154144
length,
155145
source,
146+
originalSource,
156147
recoverable,
157148
sloppy,
158149
strict;
@@ -203,8 +194,8 @@
203194
// { { ok: string } }
204195
//
205196
// therefore, scanning type expression with balancing braces.
206-
function parseType(title, last) {
207-
var ch, brace, type, direct = false;
197+
function parseType(title, last, addRange) {
198+
var ch, brace, type, startIndex, direct = false;
208199

209200

210201
// search '{'
@@ -244,6 +235,9 @@
244235
} else if (ch === 0x7B /* '{' */) {
245236
brace += 1;
246237
}
238+
if (type === '') {
239+
startIndex = index;
240+
}
247241
type += advance();
248242
}
249243
}
@@ -254,10 +248,10 @@
254248
}
255249

256250
if (isAllowedOptional(title)) {
257-
return typed.parseParamType(type);
251+
return typed.parseParamType(type, {startIndex: convertIndex(startIndex), range: addRange});
258252
}
259253

260-
return typed.parseType(type);
254+
return typed.parseType(type, {startIndex: convertIndex(startIndex), range: addRange});
261255
}
262256

263257
function scanIdentifier(last) {
@@ -402,6 +396,13 @@
402396
return true;
403397
}
404398

399+
function convertIndex(rangeIndex) {
400+
if (source === originalSource) {
401+
return rangeIndex;
402+
}
403+
return convertUnwrappedCommentIndex(originalSource, rangeIndex);
404+
}
405+
405406
function TagParser(options, title) {
406407
this._options = options;
407408
this._title = title.toLowerCase();
@@ -412,6 +413,7 @@
412413
if (this._options.lineNumbers) {
413414
this._tag.lineNumber = lineNumber;
414415
}
416+
this._first = index - title.length - 1;
415417
this._last = 0;
416418
// space to save special information for title parsers.
417419
this._extra = { };
@@ -442,7 +444,7 @@
442444
// type required titles
443445
if (isTypeParameterRequired(this._title)) {
444446
try {
445-
this._tag.type = parseType(this._title, this._last);
447+
this._tag.type = parseType(this._title, this._last, this._options.range);
446448
if (!this._tag.type) {
447449
if (!isParamTitle(this._title) && !isReturnTitle(this._title)) {
448450
if (!this.addError('Missing or invalid tag type')) {
@@ -459,7 +461,7 @@
459461
} else if (isAllowedType(this._title)) {
460462
// optional types
461463
try {
462-
this._tag.type = parseType(this._title, this._last);
464+
this._tag.type = parseType(this._title, this._last, this._options.range);
463465
} catch (e) {
464466
//For optional types, lets drop the thrown error when we hit the end of the file
465467
}
@@ -751,6 +753,10 @@
751753
// Seek to content last index.
752754
this._last = seekContent(this._title);
753755

756+
if (this._options.range) {
757+
this._tag.range = [this._first, source.slice(0, this._last).replace(/\s*$/, '').length].map(convertIndex);
758+
}
759+
754760
if (hasOwnProperty(Rules, this._title)) {
755761
sequences = Rules[this._title];
756762
} else {
@@ -831,6 +837,8 @@
831837
source = comment;
832838
}
833839

840+
originalSource = comment;
841+
834842
// array of relevant tags
835843
if (options.tags) {
836844
if (Array.isArray(options.tags)) {

0 commit comments

Comments
 (0)