Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
891c8db
feat(linter/plugins): comment-related APIs
lilnasy Oct 17, 2025
6153a88
ensure adjacency in and
lilnasy Oct 17, 2025
fbbaee4
style
lilnasy Oct 17, 2025
5dc8cac
refactor: use whitespace regex as a constant
lilnasy Oct 17, 2025
81e41db
refactor : no branching, collect slice start and end
lilnasy Oct 17, 2025
64ce2c8
refactor : no branching
lilnasy Oct 17, 2025
e6ad3d7
refactor : remove early return
lilnasy Oct 17, 2025
80a023a
Disable built-in rules in test
overlookmotel Oct 18, 2025
e6d915a
Use `range` for nodes
overlookmotel Oct 18, 2025
93bc07c
Avoid array destructuring
overlookmotel Oct 18, 2025
1822385
Move `commentsExistBetween` up to with other comments methods
overlookmotel Oct 18, 2025
4068bb8
Remove temp var in `commentsExistBetween`
overlookmotel Oct 18, 2025
b68a78c
Avoid property lookups on each turn of loop
overlookmotel Oct 18, 2025
cc0c864
Re-order code
overlookmotel Oct 18, 2025
1aadc6d
fix: Init `ast` in `commentsExistBetween`
overlookmotel Oct 18, 2025
fbdea78
rename `indexStart` to `sliceStart` with comment
lilnasy Oct 18, 2025
316d1cc
Don't use `definePlugin` in test
overlookmotel Oct 18, 2025
16bf4c9
Test output comments before/inside/after for all var and function decls
overlookmotel Oct 18, 2025
add442a
Add comment inside var decl in test fixture
overlookmotel Oct 18, 2025
d31e55f
More tests for `commentsExistBetween`
overlookmotel Oct 18, 2025
a2fbe8d
Update values of var decls in test fixture
overlookmotel Oct 18, 2025
b765da5
Test where comments before and after is empty array
overlookmotel Oct 18, 2025
3ba9484
Rename test fixture file
overlookmotel Oct 18, 2025
5a1b62b
Add test fixture containing no comments
overlookmotel Oct 18, 2025
bb26f99
Test behavior of `commentsExistBetween` when out of order
overlookmotel Oct 18, 2025
67750ee
Test statement with no comments after
overlookmotel Oct 18, 2025
3409724
Rename var
overlookmotel Oct 18, 2025
0ab0d2c
Remove temp vars
overlookmotel Oct 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 124 additions & 17 deletions apps/oxlint/src-js/plugins/source_code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import type { BufferWithArrays, Comment, Node, NodeOrToken, Ranged, Token } from

const { max } = Math;

const WHITESPACE_ONLY_REGEXP = /^\s*$/;

// Text decoder, for decoding source text from buffer
const textDecoder = new TextDecoder('utf-8', { ignoreBOM: true });

Expand Down Expand Up @@ -170,29 +172,145 @@ export const SOURCE_CODE = Object.freeze({
* @param nodeOrToken - The AST node or token to check for adjacent comment tokens.
* @returns Array of `Comment`s in occurrence order.
*/
// oxlint-disable-next-line no-unused-vars
getCommentsBefore(nodeOrToken: NodeOrToken): Comment[] {
throw new Error('`sourceCode.getCommentsBefore` not implemented yet'); // TODO
if (ast === null) initAst();

const { comments } = ast,
commentsLength = comments.length;

let targetStart = nodeOrToken.range[0]; // start

let sliceStart = commentsLength;
let sliceEnd = 0;

// Reverse iteration isn't ideal, but this entire implementation may need to be rewritten
// with token-based APIs to match eslint.
for (let i = commentsLength - 1; i >= 0; i--) {
const comment = comments[i];
const commentEnd = comment.end;

if (commentEnd < targetStart) {
const gap = sourceText.slice(commentEnd, targetStart);
if (WHITESPACE_ONLY_REGEXP.test(gap)) {
// Nothing except whitespace between end of comment and start of `nodeOrToken`
sliceStart = sliceEnd = i + 1;
targetStart = comment.start;
}
break;
}
}

for (let i = sliceEnd - 1; i >= 0; i--) {
const comment = comments[i];
const gap = sourceText.slice(comment.end, targetStart);
if (WHITESPACE_ONLY_REGEXP.test(gap)) {
// Nothing except whitespace between end of comment and start of `nodeOrToken`
sliceStart = i;
targetStart = comment.start;
} else {
break;
}
}

return comments.slice(sliceStart, sliceEnd);
},

/**
* Get all comment tokens directly after the given node or token.
* @param nodeOrToken - The AST node or token to check for adjacent comment tokens.
* @returns Array of `Comment`s in occurrence order.
*/
// oxlint-disable-next-line no-unused-vars
getCommentsAfter(nodeOrToken: NodeOrToken): Comment[] {
throw new Error('`sourceCode.getCommentsAfter` not implemented yet'); // TODO
if (ast === null) initAst();

const { comments } = ast,
commentsLength = comments.length;

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

const commentsAfter: Comment[] = [];
for (let i = 0; i < commentsLength; i++) {
const comment = comments[i],
commentStart = comment.start;

if (commentStart < targetEnd) {
continue;
}
const gap = sourceText.slice(targetEnd, commentStart);
if (WHITESPACE_ONLY_REGEXP.test(gap)) {
// Nothing except whitespace between end of `nodeOrToken` and start of comment
commentsAfter.push(comment);
targetEnd = comment.end;
} else {
break;
}
}

return commentsAfter;
},

/**
* Get all comment tokens inside the given node.
* @param node - The AST node to get the comments for.
* @returns Array of `Comment`s in occurrence order.
*/
// oxlint-disable-next-line no-unused-vars
getCommentsInside(node: Node): Comment[] {
throw new Error('`sourceCode.getCommentsInside` not implemented yet'); // TODO
if (ast === null) initAst();

const { comments } = ast,
commentsLength = comments.length;

let sliceStart = commentsLength;
let sliceEnd: number | undefined = undefined;

const { range } = node,
rangeStart = range[0],
rangeEnd = range[1];

// Linear search for first comment within `node`'s range.
// TODO: Use binary search.
for (let i = 0; i < commentsLength; i++) {
const comment = comments[i];
if (comment.start >= rangeStart) {
sliceStart = i;
break;
}
}

// Continued linear search for first comment outside `node`'s range.
// Its index is used as `sliceEnd`, which is exclusive of the slice.
for (let i = sliceStart; i < commentsLength; i++) {
const comment = comments[i];
if (comment.start > rangeEnd) {
sliceEnd = i;
break;
}
}

return comments.slice(sliceStart, sliceEnd);
},

/**
* Check whether any comments exist or not between the given 2 nodes.
* @param nodeOrToken1 - The node to check.
* @param nodeOrToken2 - The node to check.
* @returns `true` if one or more comments exist.
*/
commentsExistBetween(nodeOrToken1: NodeOrToken, nodeOrToken2: NodeOrToken): boolean {
if (ast === null) initAst();

// Find the first comment after `nodeOrToken1` ends.
// Check if it ends before `nodeOrToken2` starts.
const { comments } = ast,
commentsLength = comments.length;
const betweenRangeStart = nodeOrToken1.range[1]; // end
for (let i = 0; i < commentsLength; i++) {
const comment = comments[i];
if (comment.start >= betweenRangeStart) {
return comment.end <= nodeOrToken2.range[0]; // start
}
}
return false;
},

/**
Expand Down Expand Up @@ -463,17 +581,6 @@ export const SOURCE_CODE = Object.freeze({
getLocFromIndex: getLineColumnFromOffset,
getIndexFromLoc: getOffsetFromLineColumn,

/**
* Check whether any comments exist or not between the given 2 nodes.
* @param nodeOrToken1 - The node to check.
* @param nodeOrToken2 - The node to check.
* @returns `true` if one or more comments exist.
*/
// oxlint-disable-next-line no-unused-vars
commentsExistBetween(nodeOrToken1: NodeOrToken, nodeOrToken2: NodeOrToken): boolean {
throw new Error('`sourceCode.commentsExistBetween` not implemented yet'); // TODO
},

getAncestors,

/**
Expand Down
4 changes: 2 additions & 2 deletions apps/oxlint/test/e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ describe('oxlint CLI', () => {
}
});

it('SourceCode.getAllComments() should return all comments', async () => {
await testFixture('getAllComments');
it('should support comments-related APIs in `context.sourceCode`', async () => {
await testFixture('comments');
});
});
7 changes: 7 additions & 0 deletions apps/oxlint/test/fixtures/comments/.oxlintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"jsPlugins": ["./plugin.ts"],
"categories": { "correctness": "off" },
"rules": {
"test-comments/test-comments": "error"
}
}
32 changes: 32 additions & 0 deletions apps/oxlint/test/fixtures/comments/files/comments.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
const topLevelVariable1 = 1;
// Line comment 1
const topLevelVariable2 = 2; /* Block comment 1 */

/**
* JSDoc comment
*/
export function topLevelFunction() {
// Line comment 2
/* Block comment 2 */
let functionScopedVariable = topLevelVariable;
/**
* JSDoc comment 2
*/
function nestedFunction() {
// Line comment 3
return functionScopedVariable;
}
return nestedFunction(); // Line comment 4
}

/* Block comment 3 */
const topLevelVariable3 = /* Block comment 4 */ 3;

const topLevelVariable4 = 4;
const topLevelVariable5 = 5;

// Line comment 5
// Line comment 6

const topLevelVariable6 = 6;
const topLevelVariable7 = 7;
16 changes: 16 additions & 0 deletions apps/oxlint/test/fixtures/comments/files/no_comments.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const topLevelVariable1 = 1;
const topLevelVariable2 = 2;

export function topLevelFunction() {
let functionScopedVariable = topLevelVariable;
function nestedFunction() {
return functionScopedVariable;
}
return nestedFunction();
}

const topLevelVariable3 = 3;
const topLevelVariable4 = 4;
const topLevelVariable5 = 5;
const topLevelVariable6 = 6;
const topLevelVariable7 = 7;
Loading
Loading