Skip to content

Commit 10182e8

Browse files
authored
perf(linter/plugins): use binary search (#14778)
Part of #14564. Binary search is used to traverse the comments array wherever possible. - [x] `getCommentsBefore` - [x] `getCommentsAfter` - [x] `getCommentsInside` - [x] `commentsExistBetween`
1 parent 7c42ea0 commit 10182e8

File tree

1 file changed

+54
-46
lines changed

1 file changed

+54
-46
lines changed

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

Lines changed: 54 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -47,28 +47,22 @@ export function getCommentsBefore(nodeOrToken: NodeOrToken): Comment[] {
4747
let sliceStart = commentsLength;
4848
let sliceEnd = 0;
4949

50-
// Reverse iteration isn't ideal, but this entire implementation may need to be rewritten
51-
// with token-based APIs to match eslint.
52-
for (let i = commentsLength - 1; i >= 0; i--) {
53-
const comment = comments[i];
54-
const commentEnd = comment.end;
55-
56-
if (commentEnd < targetStart) {
57-
const gap = sourceText.slice(commentEnd, targetStart);
58-
if (WHITESPACE_ONLY_REGEXP.test(gap)) {
59-
// Nothing except whitespace between end of comment and start of `nodeOrToken`
60-
sliceStart = sliceEnd = i + 1;
61-
targetStart = comment.start;
62-
}
63-
break;
50+
// Binary search for the comment immediately before `nodeOrToken`.
51+
for (let lo = 0, hi = commentsLength; lo < hi;) {
52+
const mid = (lo + hi) >> 1;
53+
if (comments[mid].end <= targetStart) {
54+
sliceEnd = lo = mid + 1;
55+
} else {
56+
hi = mid;
6457
}
6558
}
6659

6760
for (let i = sliceEnd - 1; i >= 0; i--) {
6861
const comment = comments[i];
6962
const gap = sourceText.slice(comment.end, targetStart);
63+
// Ensure that there is nothing except whitespace between the end of the
64+
// current comment and the start of the next as we iterate backwards.
7065
if (WHITESPACE_ONLY_REGEXP.test(gap)) {
71-
// Nothing except whitespace between end of comment and start of `nodeOrToken`
7266
sliceStart = i;
7367
targetStart = comment.start;
7468
} else {
@@ -105,25 +99,33 @@ export function getCommentsAfter(nodeOrToken: NodeOrToken): Comment[] {
10599

106100
let targetEnd = nodeOrToken.range[1]; // end
107101

108-
const commentsAfter: Comment[] = [];
109-
for (let i = 0; i < commentsLength; i++) {
110-
const comment = comments[i],
111-
commentStart = comment.start;
102+
let sliceStart = commentsLength;
103+
let sliceEnd = 0;
112104

113-
if (commentStart < targetEnd) {
114-
continue;
105+
// Binary search for the comment immediately after `nodeOrToken`.
106+
for (let lo = 0, hi = commentsLength; lo < hi;) {
107+
const mid = (lo + hi) >> 1;
108+
if (comments[mid].start < targetEnd) {
109+
lo = mid + 1;
110+
} else {
111+
sliceStart = hi = mid;
115112
}
116-
const gap = sourceText.slice(targetEnd, commentStart);
113+
}
114+
115+
for (let i = sliceStart; i < commentsLength; i++) {
116+
// Ensure that there is nothing except whitespace between the
117+
// end of the previous comment and the start of the current.
118+
const comment = comments[i];
119+
const gap = sourceText.slice(targetEnd, comment.start);
117120
if (WHITESPACE_ONLY_REGEXP.test(gap)) {
118-
// Nothing except whitespace between end of `nodeOrToken` and start of comment
119-
commentsAfter.push(comment);
121+
sliceEnd = i + 1;
120122
targetEnd = comment.end;
121123
} else {
122124
break;
123125
}
124126
}
125127

126-
return commentsAfter;
128+
return comments.slice(sliceStart, sliceEnd);
127129
}
128130

129131
/**
@@ -144,23 +146,24 @@ export function getCommentsInside(node: Node): Comment[] {
144146
rangeStart = range[0],
145147
rangeEnd = range[1];
146148

147-
// Linear search for first comment within `node`'s range.
148-
// TODO: Use binary search.
149-
for (let i = 0; i < commentsLength; i++) {
150-
const comment = comments[i];
151-
if (comment.start >= rangeStart) {
152-
sliceStart = i;
153-
break;
149+
// Binary search for first comment within `node`'s range.
150+
for (let lo = 0, hi = commentsLength; lo < hi;) {
151+
const mid = (lo + hi) >> 1;
152+
if (comments[mid].start < rangeStart) {
153+
lo = mid + 1;
154+
} else {
155+
sliceStart = hi = mid;
154156
}
155157
}
156158

157-
// Continued linear search for first comment outside `node`'s range.
159+
// Binary search for first comment outside `node`'s range.
158160
// Its index is used as `sliceEnd`, which is exclusive of the slice.
159-
for (let i = sliceStart; i < commentsLength; i++) {
160-
const comment = comments[i];
161-
if (comment.start > rangeEnd) {
162-
sliceEnd = i;
163-
break;
161+
for (let lo = sliceStart, hi = commentsLength; lo < hi;) {
162+
const mid = (lo + hi) >> 1;
163+
if (comments[mid].start < rangeEnd) {
164+
lo = mid + 1;
165+
} else {
166+
sliceEnd = hi = mid;
164167
}
165168
}
166169

@@ -177,15 +180,20 @@ export function commentsExistBetween(nodeOrToken1: NodeOrToken, nodeOrToken2: No
177180
if (ast === null) initAst();
178181

179182
// Find the first comment after `nodeOrToken1` ends.
180-
// Check if it ends before `nodeOrToken2` starts.
181183
const { comments } = ast,
182-
commentsLength = comments.length;
183-
const betweenRangeStart = nodeOrToken1.range[1]; // end
184-
for (let i = 0; i < commentsLength; i++) {
185-
const comment = comments[i];
186-
if (comment.start >= betweenRangeStart) {
187-
return comment.end <= nodeOrToken2.range[0]; // start
184+
commentsLength = comments.length,
185+
betweenRangeStart = nodeOrToken1.range[1];
186+
let firstCommentBetween = -1;
187+
188+
for (let lo = 0, hi = commentsLength; lo < hi;) {
189+
const mid = (lo + hi) >> 1;
190+
if (comments[mid].start < betweenRangeStart) {
191+
lo = mid + 1;
192+
} else {
193+
firstCommentBetween = hi = mid;
188194
}
189195
}
190-
return false;
196+
// Check if it ends before `nodeOrToken2` starts.
197+
return 0 <= firstCommentBetween && firstCommentBetween < commentsLength &&
198+
comments[firstCommentBetween].end <= nodeOrToken2.range[0];
191199
}

0 commit comments

Comments
 (0)