Skip to content

Commit 93807db

Browse files
committed
feat(linter/plugins): implement SourceCode#lines property (#14290)
Add support for obtaining source text split into lines via `context.sourceCode.lines`.
1 parent 2f8c985 commit 93807db

File tree

4 files changed

+63
-6
lines changed

4 files changed

+63
-6
lines changed

apps/oxlint/src-js/plugins/source_code.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ const require = createRequire(import.meta.url);
1616

1717
const { max } = Math;
1818

19+
// Pattern for splitting source text into lines
20+
const LINE_BREAK_PATTERN = /\r\n|[\r\n\u2028\u2029]/gu;
21+
1922
// Text decoder, for decoding source text from buffer
2023
const textDecoder = new TextDecoder('utf-8', { ignoreBOM: true });
2124

@@ -31,6 +34,10 @@ let sourceText: string | null = null;
3134
let sourceByteLen: number = 0;
3235
let ast: Program | null = null;
3336

37+
// Lazily populated when `SOURCE_CODE.lines` is accessed.
38+
const lines: string[] = [],
39+
lineStartOffsets: number[] = [];
40+
3441
// Lazily populated when `SOURCE_CODE.visitorKeys` is accessed.
3542
let visitorKeys: { [key: string]: string[] } | null = null;
3643

@@ -75,6 +82,35 @@ export function getAst(): Program {
7582
return ast;
7683
}
7784

85+
/**
86+
* Split source text into lines.
87+
*/
88+
function initLines(): void {
89+
if (sourceText === null) initSourceText();
90+
91+
// This implementation is based on the one in ESLint.
92+
// TODO: Investigate if using `String.prototype.matchAll` is faster.
93+
// This comment is above ESLint's implementation:
94+
/*
95+
* Previously, this was implemented using a regex that
96+
* matched a sequence of non-linebreak characters followed by a
97+
* linebreak, then adding the lengths of the matches. However,
98+
* this caused a catastrophic backtracking issue when the end
99+
* of a file contained a large number of non-newline characters.
100+
* To avoid this, the current implementation just matches newlines
101+
* and uses match.index to get the correct line start indices.
102+
*/
103+
104+
lineStartOffsets.push(0);
105+
let lastOffset = 0, offset, match;
106+
while ((match = LINE_BREAK_PATTERN.exec(sourceText))) {
107+
offset = match.index;
108+
lines.push(sourceText.slice(lastOffset, offset));
109+
lineStartOffsets.push(lastOffset = offset + match[0].length);
110+
}
111+
lines.push(sourceText.slice(lastOffset));
112+
}
113+
78114
/**
79115
* Reset source after file has been linted, to free memory.
80116
*
@@ -89,6 +125,8 @@ export function resetSource(): void {
89125
buffer = null;
90126
sourceText = null;
91127
ast = null;
128+
lines.length = 0;
129+
lineStartOffsets.length = 0;
92130
}
93131

94132
// `SourceCode` object.
@@ -136,7 +174,8 @@ export const SOURCE_CODE = Object.freeze({
136174

137175
// Get source text as array of lines, split according to specification's definition of line breaks.
138176
get lines(): string[] {
139-
throw new Error('`sourceCode.lines` not implemented yet'); // TODO
177+
if (lines.length === 0) initLines();
178+
return lines;
140179
},
141180

142181
/**
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
11
let foo, bar;
2+
3+
// x
4+
// y

apps/oxlint/test/fixtures/sourceCode/output.snap.md

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,44 +4,51 @@
44
# stdout
55
```
66
x source-code-plugin(create): create:
7-
| text: "let foo, bar;\n"
8-
| getText(): "let foo, bar;\n"
7+
| text: "let foo, bar;\n\n// x\n// y\n"
8+
| getText(): "let foo, bar;\n\n// x\n// y\n"
9+
| lines: ["let foo, bar;","","// x","// y",""]
910
| ast: "foo"
1011
| visitorKeys: left, right
1112
,-[files/1.js:1:1]
1213
1 | let foo, bar;
1314
: ^
15+
2 |
1416
`----
1517
1618
x source-code-plugin(create-once): after:
17-
| source: "let foo, bar;\n"
19+
| source: "let foo, bar;\n\n// x\n// y\n"
1820
,-[files/1.js:1:1]
1921
1 | let foo, bar;
2022
: ^
23+
2 |
2124
`----
2225
2326
x source-code-plugin(create-once): before:
24-
| text: "let foo, bar;\n"
25-
| getText(): "let foo, bar;\n"
27+
| text: "let foo, bar;\n\n// x\n// y\n"
28+
| getText(): "let foo, bar;\n\n// x\n// y\n"
29+
| lines: ["let foo, bar;","","// x","// y",""]
2630
| ast: "foo"
2731
| visitorKeys: left, right
2832
,-[files/1.js:1:1]
2933
1 | let foo, bar;
3034
: ^
35+
2 |
3136
`----
3237
3338
x source-code-plugin(create): var decl:
3439
| source: "let foo, bar;"
3540
,-[files/1.js:1:1]
3641
1 | let foo, bar;
3742
: ^^^^^^^^^^^^^
43+
2 |
3844
`----
3945
4046
x source-code-plugin(create-once): var decl:
4147
| source: "let foo, bar;"
4248
,-[files/1.js:1:1]
4349
1 | let foo, bar;
4450
: ^^^^^^^^^^^^^
51+
2 |
4552
`----
4653
4754
x source-code-plugin(create): ident "foo":
@@ -52,6 +59,7 @@
5259
,-[files/1.js:1:5]
5360
1 | let foo, bar;
5461
: ^^^
62+
2 |
5563
`----
5664
5765
x source-code-plugin(create-once): ident "foo":
@@ -62,6 +70,7 @@
6270
,-[files/1.js:1:5]
6371
1 | let foo, bar;
6472
: ^^^
73+
2 |
6574
`----
6675
6776
x source-code-plugin(create): ident "bar":
@@ -72,6 +81,7 @@
7281
,-[files/1.js:1:10]
7382
1 | let foo, bar;
7483
: ^^^
84+
2 |
7585
`----
7686
7787
x source-code-plugin(create-once): ident "bar":
@@ -82,11 +92,13 @@
8292
,-[files/1.js:1:10]
8393
1 | let foo, bar;
8494
: ^^^
95+
2 |
8596
`----
8697
8798
x source-code-plugin(create): create:
8899
| text: "let qux;\n"
89100
| getText(): "let qux;\n"
101+
| lines: ["let qux;",""]
90102
| ast: "qux"
91103
| visitorKeys: left, right
92104
,-[files/2.js:1:1]
@@ -104,6 +116,7 @@
104116
x source-code-plugin(create-once): before:
105117
| text: "let qux;\n"
106118
| getText(): "let qux;\n"
119+
| lines: ["let qux;",""]
107120
| ast: "qux"
108121
| visitorKeys: left, right
109122
,-[files/2.js:1:1]

apps/oxlint/test/fixtures/sourceCode/plugin.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const createRule: Rule = {
1313
message: 'create:\n' +
1414
`text: ${JSON.stringify(context.sourceCode.text)}\n` +
1515
`getText(): ${JSON.stringify(context.sourceCode.getText())}\n` +
16+
`lines: ${JSON.stringify(context.sourceCode.lines)}\n` +
1617
// @ts-ignore
1718
`ast: "${ast.body[0].declarations[0].id.name}"\n` +
1819
`visitorKeys: ${context.sourceCode.visitorKeys.BinaryExpression.join(', ')}`,
@@ -55,6 +56,7 @@ const createOnceRule: Rule = {
5556
message: 'before:\n' +
5657
`text: ${JSON.stringify(context.sourceCode.text)}\n` +
5758
`getText(): ${JSON.stringify(context.sourceCode.getText())}\n` +
59+
`lines: ${JSON.stringify(context.sourceCode.lines)}\n` +
5860
// @ts-ignore
5961
`ast: "${ast.body[0].declarations[0].id.name}"\n` +
6062
`visitorKeys: ${context.sourceCode.visitorKeys.BinaryExpression.join(', ')}`,

0 commit comments

Comments
 (0)