feat(unstable): support comments in lint plugin (#29189)

This PR adds support for comments in the AST for lint plugins.

- The `Program` node has a `comments` field pointing to an array of all
comments.
- `SourceCode.getAllComments()`: Returns all comments (same as
`program.comments`)
- `SourceCode.getCommentsBefore(node)`: Get all comments before this
Node
- `SourceCode.getCommentsAfter(node)`: Get all comments after this Node
- `SourceCode.getCommentsInside(node)`: Get all comments inside this
Node

ESLint docs:
-
https://eslint.org/docs/latest/extend/custom-rules#accessing-the-source-code
- https://eslint.org/docs/latest/extend/custom-rules#accessing-comments
This commit is contained in:
Marvin Hagemeister 2025-05-08 21:59:36 +02:00 committed by GitHub
parent e1e67a703c
commit c015b8affd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 352 additions and 7 deletions

View file

@ -263,6 +263,79 @@ export class SourceCode {
return ancestors;
}
/**
* @returns {Array<Deno.lint.LineComment | Deno.lint.BlockComment>}
*/
getAllComments() {
materializeComments(this.#ctx);
return this.#ctx.comments;
}
/**
* @param {Deno.lint.Node} node
* @returns {Array<Deno.lint.LineComment | Deno.lint.BlockComment>}
*/
getCommentsBefore(node) {
materializeComments(this.#ctx);
/** @type {Array<Deno.lint.LineComment | Deno.lint.BlockComment>} */
const before = [];
const { comments } = this.#ctx;
for (let i = 0; i < comments.length; i++) {
const comment = comments[i];
if (comment.range[0] <= node.range[0]) {
before.push(comment);
}
}
return before;
}
/**
* @param {Deno.lint.Node} node
* @returns {Array<Deno.lint.LineComment | Deno.lint.BlockComment>}
*/
getCommentsAfter(node) {
materializeComments(this.#ctx);
/** @type {Array<Deno.lint.LineComment | Deno.lint.BlockComment>} */
const after = [];
const { comments } = this.#ctx;
for (let i = 0; i < comments.length; i++) {
const comment = comments[i];
if (comment.range[0] >= node.range[1]) {
after.push(comment);
}
}
return after;
}
/**
* @param {Deno.lint.Node} node
* @returns {Array<Deno.lint.LineComment | Deno.lint.BlockComment>}
*/
getCommentsInside(node) {
materializeComments(this.#ctx);
/** @type {Array<Deno.lint.LineComment | Deno.lint.BlockComment>} */
const inside = [];
const { comments } = this.#ctx;
for (let i = 0; i < comments.length; i++) {
const comment = comments[i];
if (
comment.range[0] >= node.range[0] && comment.range[1] <= node.range[1]
) {
inside.push(comment);
}
}
return inside;
}
/**
* @returns {string}
*/
@ -345,6 +418,34 @@ export class Context {
}
}
/**
* @param {AstContext} ctx
*/
function materializeComments(ctx) {
const { buf, commentsOffset, comments, strTable } = ctx;
let offset = commentsOffset;
const count = readU32(buf, offset);
offset += 4;
if (comments.length === count) return;
while (offset < buf.length && comments.length < count) {
const kind = buf[offset];
offset++;
const spanId = readU32(buf, offset);
offset += 4;
const strId = readU32(buf, offset);
offset += 4;
comments.push({
type: kind === 0 ? "Line" : "Block",
range: readSpan(ctx, spanId),
value: getString(strTable, strId),
});
}
}
/**
* @param {Deno.lint.Plugin[]} plugins
* @param {string[]} exclude
@ -489,6 +590,7 @@ class FacadeNode {
/** @type {Set<number>} */
const appliedGetters = new Set();
let hasCommenstGetter = false;
/**
* Add getters for all potential properties found in the message.
@ -515,6 +617,16 @@ function setNodeGetters(ctx) {
},
});
}
if (!hasCommenstGetter) {
hasCommenstGetter = true;
Object.defineProperty(FacadeNode.prototype, "comments", {
get() {
materializeComments(ctx);
return ctx.comments;
},
});
}
}
/**
@ -994,6 +1106,7 @@ function createAstContext(buf, token) {
// The buffer has a few offsets at the end which allows us to easily
// jump to the relevant sections of the message.
const commentsOffset = readU32(buf, buf.length - 28);
const propsOffset = readU32(buf, buf.length - 24);
const spansOffset = readU32(buf, buf.length - 20);
const typeMapOffset = readU32(buf, buf.length - 16);
@ -1060,7 +1173,9 @@ function createAstContext(buf, token) {
rootOffset,
spansOffset,
propsOffset,
commentsOffset,
nodes: new Map(),
comments: [],
strTableOffset,
strByProp,
strByType,

View file

@ -8,6 +8,8 @@ export interface AstContext {
nodes: Map<number, Deno.lint.Node>;
spansOffset: number;
propsOffset: number;
commentsOffset: number;
comments: Array<Deno.lint.LineComment | Deno.lint.BlockComment>;
strByType: number[];
strByProp: number[];
typeByStr: Map<string, number>;

View file

@ -142,6 +142,19 @@ struct Node {
parent: u32,
}
#[derive(Debug)]
pub enum CommentKind {
Line,
Block,
}
#[derive(Debug)]
struct Comment {
kind: CommentKind,
str_id: usize,
span_id: usize,
}
#[derive(Debug)]
pub struct SerializeCtx {
root_idx: Index,
@ -161,6 +174,9 @@ pub struct SerializeCtx {
kind_name_map: Vec<usize>,
/// Maps prop id to string id
prop_name_map: Vec<usize>,
/// Comments
comments: Vec<Comment>,
}
/// This is the internal context used to allocate and fill the buffer. The point
@ -185,6 +201,7 @@ impl SerializeCtx {
str_table: StringTable::new(),
kind_name_map: vec![0; kind_size],
prop_name_map: vec![0; prop_size],
comments: vec![],
};
let empty_str = ctx.str_table.insert("");
@ -285,12 +302,7 @@ impl SerializeCtx {
where
K: Into<u8> + Display + Clone,
{
let (start, end) = if *span == DUMMY_SP {
(0, 0)
} else {
// -1 is because swc stores spans 1-indexed
(span.lo.0 - 1, span.hi.0 - 1)
};
let (start, end) = span_to_value(span);
self.append_inner(kind, start, end)
}
@ -559,6 +571,21 @@ impl SerializeCtx {
self.write_ref_vec(prop, parent_ref, actual)
}
pub fn write_comment(&mut self, kind: CommentKind, value: &str, span: &Span) {
let str_id = self.str_table.insert(value);
let span_id = self.spans.len() / 2;
let (span_lo, span_hi) = span_to_value(span);
self.spans.push(span_lo);
self.spans.push(span_hi);
self.comments.push(Comment {
kind,
str_id,
span_id,
});
}
/// Serialize all information we have into a buffer that can be sent to JS.
/// It has the following structure:
///
@ -629,10 +656,24 @@ impl SerializeCtx {
let offset_props = buf.len();
buf.append(&mut self.field_buf);
// Serialize comments
let offset_comments = buf.len();
append_usize(&mut buf, self.comments.len());
for comment in &self.comments {
let kind = match comment.kind {
CommentKind::Line => 0,
CommentKind::Block => 1,
};
buf.push(kind);
append_usize(&mut buf, comment.span_id);
append_usize(&mut buf, comment.str_id);
}
// Putting offsets of relevant parts of the buffer at the end. This
// allows us to hop to the relevant part by merely looking at the last
// for values in the message. Each value represents an offset into the
// buffer.
append_usize(&mut buf, offset_comments);
append_usize(&mut buf, offset_props);
append_usize(&mut buf, offset_spans);
append_usize(&mut buf, offset_kind_map);
@ -643,3 +684,12 @@ impl SerializeCtx {
buf
}
}
fn span_to_value(span: &Span) -> (u32, u32) {
if *span == DUMMY_SP {
(0, 0)
} else {
// -1 is because swc stores spans 1-indexed
(span.lo.0 - 1, span.hi.0 - 1)
}
}

View file

@ -90,6 +90,7 @@ use deno_ast::view::VarDeclKind;
use deno_ast::ParsedSource;
use super::buffer::AstBufSerializer;
use super::buffer::CommentKind;
use super::buffer::NodeRef;
use super::ts_estree::AstNode;
use super::ts_estree::MethodKind as TsEstreeMethodKind;
@ -134,6 +135,14 @@ pub fn serialize_swc_to_buffer(
}
}
for comment in parsed_source.comments().get_vec() {
let kind = match comment.kind {
deno_ast::swc::common::comments::CommentKind::Line => CommentKind::Line,
deno_ast::swc::common::comments::CommentKind::Block => CommentKind::Block,
};
ctx.write_comment(kind, &comment.text, &comment.span);
}
ctx.map_utf8_spans_to_utf16(utf16_map);
ctx.serialize()
}

View file

@ -8,6 +8,7 @@ use deno_ast::swc::common::Span;
use deno_ast::view::TruePlusMinus;
use super::buffer::AstBufSerializer;
use super::buffer::CommentKind;
use super::buffer::NodeRef;
use super::buffer::SerializeCtx;
use crate::util::text_encoding::Utf16Map;
@ -2890,6 +2891,10 @@ impl TsEsTreeBuilder {
_ => self.ctx.write_undefined(prop),
}
}
pub fn write_comment(&mut self, kind: CommentKind, value: &str, span: &Span) {
self.ctx.write_comment(kind, value, span);
}
}
#[derive(Debug)]

View file

@ -1406,6 +1406,27 @@ declare namespace Deno {
* current node.
*/
getAncestors(node: Node): Node[];
/**
* Get all comments inside the source.
*/
getAllComments(): Array<LineComment | BlockComment>;
/**
* Get leading comments before a node.
*/
getCommentsBefore(node: Node): Array<LineComment | BlockComment>;
/**
* Get trailing comments after a node.
*/
getCommentsAfter(node: Node): Array<LineComment | BlockComment>;
/**
* Get comments inside a node.
*/
getCommentsInside(node: Node): Array<LineComment | BlockComment>;
/**
* Get the full source code.
*/
@ -1532,6 +1553,7 @@ declare namespace Deno {
range: Range;
sourceType: "module" | "script";
body: Statement[];
comments: Array<LineComment | BlockComment>;
}
/**
@ -4335,6 +4357,28 @@ declare namespace Deno {
| TSUnknownKeyword
| TSVoidKeyword;
/**
* A single line comment
* @category Linter
* @experimental
*/
export interface LineComment {
type: "Line";
range: Range;
value: string;
}
/**
* A potentially multi-line block comment
* @category Linter
* @experimental
*/
export interface BlockComment {
type: "Block";
range: Range;
value: string;
}
/**
* Union type of all possible AST nodes
* @category Linter
@ -4394,7 +4438,9 @@ declare namespace Deno {
| TSIndexSignature
| TSTypeAnnotation
| TSTypeParameterDeclaration
| TSTypeParameter;
| TSTypeParameter
| LineComment
| BlockComment;
export {}; // only export exports
}

View file

@ -0,0 +1,6 @@
{
"tempDir": true,
"args": "lint main.ts",
"output": "comments.out",
"exitCode": 0
}

View file

@ -0,0 +1,67 @@
{
program: [
{ type: "Line", range: [ 0, 14 ], value: " before line" },
{
type: "Block",
range: [ 15, 38 ],
value: "*\n * before block\n "
},
{ type: "Line", range: [ 58, 72 ], value: " inside line" },
{
type: "Block",
range: [ 75, 102 ],
value: "*\n * inside block\n "
},
{ type: "Line", range: [ 106, 119 ], value: " after line" },
{
type: "Block",
range: [ 120, 142 ],
value: "*\n * after block\n "
}
],
all: [
{ type: "Line", range: [ 0, 14 ], value: " before line" },
{
type: "Block",
range: [ 15, 38 ],
value: "*\n * before block\n "
},
{ type: "Line", range: [ 58, 72 ], value: " inside line" },
{
type: "Block",
range: [ 75, 102 ],
value: "*\n * inside block\n "
},
{ type: "Line", range: [ 106, 119 ], value: " after line" },
{
type: "Block",
range: [ 120, 142 ],
value: "*\n * after block\n "
}
],
before: [
{ type: "Line", range: [ 0, 14 ], value: " before line" },
{
type: "Block",
range: [ 15, 38 ],
value: "*\n * before block\n "
}
],
after: [
{ type: "Line", range: [ 106, 119 ], value: " after line" },
{
type: "Block",
range: [ 120, 142 ],
value: "*\n * after block\n "
}
],
inside: [
{ type: "Line", range: [ 58, 72 ], value: " inside line" },
{
type: "Block",
range: [ 75, 102 ],
value: "*\n * inside block\n "
}
]
}
Checked 1 file

View file

@ -0,0 +1,5 @@
{
"lint": {
"plugins": ["./plugin.ts"]
}
}

View file

@ -0,0 +1,17 @@
// before line
/**
* before block
*/
function foo() {
// inside line
/**
* inside block
*/
}
// after line
/**
* after block
*/
foo();

View file

@ -0,0 +1,23 @@
export default {
name: "foo",
rules: {
foo: {
create(ctx) {
let program: Array<Deno.lint.LineComment | Deno.lint.BlockComment> = [];
return {
Program(node) {
program = node.comments;
},
FunctionDeclaration(node) {
const all = ctx.sourceCode.getAllComments();
const before = ctx.sourceCode.getCommentsBefore(node);
const after = ctx.sourceCode.getCommentsAfter(node);
const inside = ctx.sourceCode.getCommentsInside(node);
console.log({ program, all, before, after, inside });
},
};
},
},
},
} satisfies Deno.lint.Plugin;