|
10 | 10 |
|
11 | 11 | const TokenStore = require("../token-store"), |
12 | 12 | Traverser = require("./traverser"), |
13 | | - astUtils = require("../ast-utils"); |
| 13 | + astUtils = require("../ast-utils"), |
| 14 | + lodash = require("lodash"); |
14 | 15 |
|
15 | 16 | //------------------------------------------------------------------------------ |
16 | 17 | // Private |
@@ -138,7 +139,26 @@ function SourceCode(text, ast) { |
138 | 139 | * This is done to avoid each rule needing to do so separately. |
139 | 140 | * @type string[] |
140 | 141 | */ |
141 | | - this.lines = SourceCode.splitLines(this.text); |
| 142 | + this.lines = []; |
| 143 | + this.lineStartIndices = [0]; |
| 144 | + |
| 145 | + const lineEndingPattern = astUtils.createGlobalLinebreakMatcher(); |
| 146 | + let match; |
| 147 | + |
| 148 | + /* |
| 149 | + * Previously, this was implemented using a regex that |
| 150 | + * matched a sequence of non-linebreak characters followed by a |
| 151 | + * linebreak, then adding the lengths of the matches. However, |
| 152 | + * this caused a catastrophic backtracking issue when the end |
| 153 | + * of a file contained a large number of non-newline characters. |
| 154 | + * To avoid this, the current implementation just matches newlines |
| 155 | + * and uses match.index to get the correct line start indices. |
| 156 | + */ |
| 157 | + while ((match = lineEndingPattern.exec(this.text))) { |
| 158 | + this.lines.push(this.text.slice(this.lineStartIndices[this.lineStartIndices.length - 1], match.index)); |
| 159 | + this.lineStartIndices.push(match.index + match[0].length); |
| 160 | + } |
| 161 | + this.lines.push(this.text.slice(this.lineStartIndices[this.lineStartIndices.length - 1])); |
142 | 162 |
|
143 | 163 | this.tokensAndComments = sortedMerge(ast.tokens, ast.comments); |
144 | 164 |
|
@@ -312,6 +332,83 @@ SourceCode.prototype = { |
312 | 332 | const text = this.text.slice(first.range[1], second.range[0]); |
313 | 333 |
|
314 | 334 | return /\s/.test(text.replace(/\/\*.*?\*\//g, "")); |
| 335 | + }, |
| 336 | + |
| 337 | + /** |
| 338 | + * Converts a source text index into a (line, column) pair. |
| 339 | + * @param {number} index The index of a character in a file |
| 340 | + * @returns {Object} A {line, column} location object with a 0-indexed column |
| 341 | + */ |
| 342 | + getLocFromIndex(index) { |
| 343 | + if (typeof index !== "number") { |
| 344 | + throw new TypeError("Expected `index` to be a number."); |
| 345 | + } |
| 346 | + |
| 347 | + if (index < 0 || index > this.text.length) { |
| 348 | + throw new RangeError(`Index out of range (requested index ${index}, but source text has length ${this.text.length}).`); |
| 349 | + } |
| 350 | + |
| 351 | + /* |
| 352 | + * For an argument of this.text.length, return the location one "spot" past the last character |
| 353 | + * of the file. If the last character is a linebreak, the location will be column 0 of the next |
| 354 | + * line; otherwise, the location will be in the next column on the same line. |
| 355 | + * |
| 356 | + * See getIndexFromLoc for the motivation for this special case. |
| 357 | + */ |
| 358 | + if (index === this.text.length) { |
| 359 | + return { line: this.lines.length, column: this.lines[this.lines.length - 1].length }; |
| 360 | + } |
| 361 | + |
| 362 | + /* |
| 363 | + * To figure out which line rangeIndex is on, determine the last index at which rangeIndex could |
| 364 | + * be inserted into lineIndices to keep the list sorted. |
| 365 | + */ |
| 366 | + const lineNumber = lodash.sortedLastIndex(this.lineStartIndices, index); |
| 367 | + |
| 368 | + return { line: lineNumber, column: index - this.lineStartIndices[lineNumber - 1] }; |
| 369 | + |
| 370 | + }, |
| 371 | + |
| 372 | + /** |
| 373 | + * Converts a (line, column) pair into a range index. |
| 374 | + * @param {Object} loc A line/column location |
| 375 | + * @param {number} loc.line The line number of the location (1-indexed) |
| 376 | + * @param {number} loc.column The column number of the location (0-indexed) |
| 377 | + * @returns {number} The range index of the location in the file. |
| 378 | + */ |
| 379 | + getIndexFromLoc(loc) { |
| 380 | + if (typeof loc !== "object" || typeof loc.line !== "number" || typeof loc.column !== "number") { |
| 381 | + throw new TypeError("Expected `loc` to be an object with numeric `line` and `column` properties."); |
| 382 | + } |
| 383 | + |
| 384 | + if (loc.line <= 0) { |
| 385 | + throw new RangeError(`Line number out of range (line ${loc.line} requested). Line numbers should be 1-based.`); |
| 386 | + } |
| 387 | + |
| 388 | + if (loc.line > this.lineStartIndices.length) { |
| 389 | + throw new RangeError(`Line number out of range (line ${loc.line} requested, but only ${this.lineStartIndices.length} lines present).`); |
| 390 | + } |
| 391 | + |
| 392 | + const lineStartIndex = this.lineStartIndices[loc.line - 1]; |
| 393 | + const lineEndIndex = loc.line === this.lineStartIndices.length ? this.text.length : this.lineStartIndices[loc.line]; |
| 394 | + const positionIndex = lineStartIndex + loc.column; |
| 395 | + |
| 396 | + /* |
| 397 | + * By design, getIndexFromLoc({ line: lineNum, column: 0 }) should return the start index of |
| 398 | + * the given line, provided that the line number is valid element of this.lines. Since the |
| 399 | + * last element of this.lines is an empty string for files with trailing newlines, add a |
| 400 | + * special case where getting the index for the first location after the end of the file |
| 401 | + * will return the length of the file, rather than throwing an error. This allows rules to |
| 402 | + * use getIndexFromLoc consistently without worrying about edge cases at the end of a file. |
| 403 | + */ |
| 404 | + if ( |
| 405 | + loc.line === this.lineStartIndices.length && positionIndex > lineEndIndex || |
| 406 | + loc.line < this.lineStartIndices.length && positionIndex >= lineEndIndex |
| 407 | + ) { |
| 408 | + throw new RangeError(`Column number out of range (column ${loc.column} requested, but the length of line ${loc.line} is ${lineEndIndex - lineStartIndex}).`); |
| 409 | + } |
| 410 | + |
| 411 | + return positionIndex; |
315 | 412 | } |
316 | 413 | }; |
317 | 414 |
|
|
0 commit comments