1 | // @ts-check
|
2 |
|
3 | /**
|
4 | * Gets info about a source code range, including line/column numbers and if
|
5 | * it’s ignored by a comment.
|
6 | * @param {string} source Source code.
|
7 | * @param {number} startOffset Start character offset.
|
8 | * @param {number} endOffset End character offset.
|
9 | * @param {string | false} [ignoreNextLineComment] Single line
|
10 | * case-insensitive comment content to ignore ranges that start on the the
|
11 | * next line, or `false` to disable ignore comments. Defaults to
|
12 | * `" coverage ignore next line"`.
|
13 | * @returns {SourceCodeRange} Source code range info.
|
14 | */
|
15 | export default function sourceRange(
|
16 | source,
|
17 | startOffset,
|
18 | endOffset,
|
19 | ignoreNextLineComment = " coverage ignore next line"
|
20 | ) {
|
21 | if (typeof source !== "string")
|
22 | throw new TypeError("Argument 1 `source` must be a string.");
|
23 |
|
24 | if (typeof startOffset !== "number")
|
25 | throw new TypeError("Argument 2 `startOffset` must be a number.");
|
26 |
|
27 | if (typeof endOffset !== "number")
|
28 | throw new TypeError("Argument 3 `endOffset` must be a number.");
|
29 |
|
30 | if (
|
31 | typeof ignoreNextLineComment !== "string" &&
|
32 | ignoreNextLineComment !== false
|
33 | )
|
34 | throw new TypeError(
|
35 | "Argument 4 `ignoreNextLineComment` must be a string or `false`."
|
36 | );
|
37 |
|
38 | const ignoreNextLineCommentLowerCase = ignoreNextLineComment
|
39 | ? `//${ignoreNextLineComment.toLowerCase()}`
|
40 | : null;
|
41 |
|
42 | /** @type {SourceCodeRange["ignore"]} */
|
43 | let ignore = false;
|
44 |
|
45 | /** @type {SourceCodeLocation["line"] | undefined} */
|
46 | let startLine;
|
47 |
|
48 | /** @type {SourceCodeLocation["column"] | undefined} */
|
49 | let startColumn;
|
50 |
|
51 | /** @type {SourceCodeLocation["line"] | undefined} */
|
52 | let endLine;
|
53 |
|
54 | /** @type {SourceCodeLocation["column"] | undefined} */
|
55 | let endColumn;
|
56 |
|
57 | const lines = source.split(/^/gmu);
|
58 |
|
59 | let lineOffset = 0;
|
60 |
|
61 | for (const [lineIndex, lineSource] of lines.entries()) {
|
62 | const nextLineOffset = lineOffset + lineSource.length;
|
63 |
|
64 | if (
|
65 | !startLine &&
|
66 | startOffset >= lineOffset &&
|
67 | startOffset < nextLineOffset
|
68 | ) {
|
69 | startLine = lineIndex + 1;
|
70 | startColumn = startOffset - lineOffset + 1;
|
71 |
|
72 | if (
|
73 | // Ignoring is enabled.
|
74 | ignoreNextLineCommentLowerCase &&
|
75 | // It’s not the first line that can’t be ignored, because there can’t be
|
76 | // an ignore comment on the previous line.
|
77 | lineIndex &&
|
78 | // The previous line contains the case-insensitive comment to ignore
|
79 | // this line.
|
80 | lines[lineIndex - 1]
|
81 | .trim()
|
82 | .toLowerCase()
|
83 | .endsWith(ignoreNextLineCommentLowerCase)
|
84 | )
|
85 | ignore = true;
|
86 | }
|
87 |
|
88 | if (endOffset >= lineOffset && endOffset < nextLineOffset) {
|
89 | endLine = lineIndex + 1;
|
90 | endColumn = endOffset - lineOffset + 1;
|
91 | break;
|
92 | }
|
93 |
|
94 | lineOffset = nextLineOffset;
|
95 | }
|
96 |
|
97 | return {
|
98 | ignore,
|
99 | start: {
|
100 | offset: startOffset,
|
101 | line: /** @type {number} */ (startLine),
|
102 | column: /** @type {number} */ (startColumn),
|
103 | },
|
104 | end: {
|
105 | offset: endOffset,
|
106 | line: /** @type {number} */ (endLine),
|
107 | column: /** @type {number} */ (endColumn),
|
108 | },
|
109 | };
|
110 | }
|
111 |
|
112 | /**
|
113 | * Source code location.
|
114 | * @typedef {object} SourceCodeLocation
|
115 | * @prop {number} offset Character offset.
|
116 | * @prop {number} line Line number.
|
117 | * @prop {number} column Column number.
|
118 | */
|
119 |
|
120 | /**
|
121 | * Source code range details.
|
122 | * @typedef {object} SourceCodeRange
|
123 | * @prop {boolean} ignore Should it be ignored.
|
124 | * @prop {SourceCodeLocation} start Start location.
|
125 | * @prop {SourceCodeLocation} end End location.
|
126 | */
|