use rustc_lexer::{FrontmatterAllowed, TokenKind, tokenize};
use rustc_lint::{LateContext, LintContext};
use rustc_span::def_id::LOCAL_CRATE;
use rustc_span::{BytePos, Pos, RelativeBytePos, SourceFile, Span, SyntaxContext};
use crate::module_reparse::crate_module_files;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum CommentSurface {
DocBlock,
DocBlockBlock,
PlainLine,
PlainBlock,
}
pub(crate) struct CommentChunk<'a> {
pub(crate) surface: CommentSurface,
pub(crate) rendered: String,
pub(crate) lines: Vec<LineMapping>,
pub(crate) source_span: Span,
pub(crate) source_file: &'a SourceFile,
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct LineMapping {
pub(crate) rendered_start: usize,
pub(crate) rendered_len: usize,
pub(crate) source_offset: u32,
}
impl CommentChunk<'_> {
pub(crate) fn span_for(&self, rendered_offset: usize, len: u32) -> Option<Span> {
for line in &self.lines {
if rendered_offset >= line.rendered_start
&& rendered_offset < line.rendered_start + line.rendered_len
{
let delta = (rendered_offset - line.rendered_start) as u32;
let start = self
.source_file
.absolute_position(RelativeBytePos::from_u32(line.source_offset + delta));
let end = BytePos::from_u32(start.0 + len);
return Some(Span::new(start, end, SyntaxContext::root(), None));
}
}
None
}
}
pub(crate) fn walk_local_comments(
lint_context: &LateContext<'_>,
mut callback: impl FnMut(&CommentChunk<'_>),
) {
let module_files = crate_module_files(lint_context);
let source_map = lint_context.sess().source_map();
for source_file in source_map.files().iter() {
if source_file.cnum != LOCAL_CRATE {
continue;
}
if !module_files.contains(&source_file.name) {
continue;
}
let Some(source_text) = source_file.src.as_deref() else {
continue;
};
walk_source_file(source_text, source_file, &mut callback);
}
}
fn walk_source_file<'a>(
source_text: &'a str,
source_file: &'a SourceFile,
callback: &mut dyn FnMut(&CommentChunk<'_>),
) {
let mut tokens: Vec<(u32, u32, TokenKind)> = Vec::new();
let mut offset: u32 = 0;
for token in tokenize(source_text, FrontmatterAllowed::Yes) {
let start = offset;
let len = token.len;
let end = start
.checked_add(len)
.expect("source-file offset overflowed");
tokens.push((start, end, token.kind));
offset = end;
}
let mut index = 0;
while index < tokens.len() {
let (start, end, kind) = tokens[index];
match kind {
TokenKind::LineComment { doc_style: Some(_) } => {
let (chunk, consumed) =
gather_line_doc_comments(&tokens, index, source_text, source_file);
callback(&chunk);
index += consumed;
}
TokenKind::LineComment { doc_style: None } => {
let (chunk, consumed) =
gather_line_plain_comments(&tokens, index, source_text, source_file);
callback(&chunk);
index += consumed;
}
TokenKind::BlockComment {
doc_style: Some(_), ..
} => {
let chunk = build_block_doc_comment(source_text, source_file, start, end);
callback(&chunk);
index += 1;
}
TokenKind::BlockComment {
doc_style: None, ..
} => {
let chunk = build_block_plain_comment(source_text, source_file, start, end);
callback(&chunk);
index += 1;
}
_ => index += 1,
}
}
}
fn gather_line_doc_comments<'a>(
tokens: &[(u32, u32, TokenKind)],
start_idx: usize,
source_text: &'a str,
source_file: &'a SourceFile,
) -> (CommentChunk<'a>, usize) {
let mut idx = start_idx;
let initial_doc_style = match tokens[start_idx].2 {
TokenKind::LineComment {
doc_style: Some(style),
} => Some(style),
_ => None,
};
let mut last_doc_end = tokens[start_idx].1;
let mut consumed = 1;
idx += 1;
while idx < tokens.len() {
match tokens[idx].2 {
TokenKind::Whitespace => {
idx += 1;
}
TokenKind::LineComment { doc_style: Some(s) } if Some(s) == initial_doc_style => {
last_doc_end = tokens[idx].1;
idx += 1;
consumed = idx - start_idx;
}
_ => break,
}
}
let block_start = tokens[start_idx].0;
let block_end = last_doc_end;
let block_src = &source_text[block_start as usize..block_end as usize];
let (rendered, lines) = render_line_doc_block(block_src, block_start);
let span = Span::new(
source_file.absolute_position(RelativeBytePos::from_u32(block_start)),
source_file.absolute_position(RelativeBytePos::from_u32(block_end)),
SyntaxContext::root(),
None,
);
let chunk = CommentChunk {
surface: CommentSurface::DocBlock,
rendered,
lines,
source_span: span,
source_file,
};
(chunk, consumed)
}
fn gather_line_plain_comments<'a>(
tokens: &[(u32, u32, TokenKind)],
start_idx: usize,
source_text: &'a str,
source_file: &'a SourceFile,
) -> (CommentChunk<'a>, usize) {
let mut idx = start_idx + 1;
let mut last_end = tokens[start_idx].1;
let mut consumed = 1;
while idx < tokens.len() {
match tokens[idx].2 {
TokenKind::Whitespace => idx += 1,
TokenKind::LineComment { doc_style: None } => {
last_end = tokens[idx].1;
idx += 1;
consumed = idx - start_idx;
}
_ => break,
}
}
let block_start = tokens[start_idx].0;
let block_end = last_end;
let block_src = &source_text[block_start as usize..block_end as usize];
let (rendered, lines) = render_line_plain_block(block_src, block_start);
let span = Span::new(
source_file.absolute_position(RelativeBytePos::from_u32(block_start)),
source_file.absolute_position(RelativeBytePos::from_u32(block_end)),
SyntaxContext::root(),
None,
);
let chunk = CommentChunk {
surface: CommentSurface::PlainLine,
rendered,
lines,
source_span: span,
source_file,
};
(chunk, consumed)
}
fn build_block_doc_comment<'a>(
source_text: &'a str,
source_file: &'a SourceFile,
start: u32,
end: u32,
) -> CommentChunk<'a> {
let body_text = &source_text[start as usize..end as usize];
let open = if body_text.starts_with("/*!") {
"/*!"
} else {
"/**"
};
let (rendered, lines) = render_block_comment(body_text, start, open, "*/");
let span = Span::new(
source_file.absolute_position(RelativeBytePos::from_u32(start)),
source_file.absolute_position(RelativeBytePos::from_u32(end)),
SyntaxContext::root(),
None,
);
CommentChunk {
surface: CommentSurface::DocBlockBlock,
rendered,
lines,
source_span: span,
source_file,
}
}
fn build_block_plain_comment<'a>(
source_text: &'a str,
source_file: &'a SourceFile,
start: u32,
end: u32,
) -> CommentChunk<'a> {
let body_text = &source_text[start as usize..end as usize];
let (rendered, lines) = render_block_comment(body_text, start, "/*", "*/");
let span = Span::new(
source_file.absolute_position(RelativeBytePos::from_u32(start)),
source_file.absolute_position(RelativeBytePos::from_u32(end)),
SyntaxContext::root(),
None,
);
CommentChunk {
surface: CommentSurface::PlainBlock,
rendered,
lines,
source_span: span,
source_file,
}
}
fn render_line_doc_block(block_src: &str, block_source_start: u32) -> (String, Vec<LineMapping>) {
let mut rendered = String::with_capacity(block_src.len());
let mut lines: Vec<LineMapping> = Vec::new();
let mut offset_in_block: u32 = 0;
for raw_line in block_src.split_inclusive('\n') {
let has_newline = raw_line.ends_with('\n');
let line_content = raw_line.strip_suffix('\n').unwrap_or(raw_line);
let line_content = line_content.strip_suffix('\r').unwrap_or(line_content);
let bytes = line_content.as_bytes();
let indent = bytes
.iter()
.take_while(|&&byte| byte == b' ' || byte == b'\t')
.count();
if !bytes[indent..].starts_with(b"//") {
offset_in_block += raw_line.len() as u32;
continue;
}
let mut prefix_end = indent + 2;
if prefix_end < bytes.len() && (bytes[prefix_end] == b'/' || bytes[prefix_end] == b'!') {
prefix_end += 1;
}
let mut content_start = prefix_end;
if content_start < bytes.len() && bytes[content_start] == b' ' {
content_start += 1;
}
let content = &line_content[content_start..];
let rendered_start = rendered.len();
rendered.push_str(content);
let line_source_offset = block_source_start + offset_in_block + content_start as u32;
lines.push(LineMapping {
rendered_start,
rendered_len: content.len(),
source_offset: line_source_offset,
});
if has_newline {
rendered.push('\n');
}
offset_in_block += raw_line.len() as u32;
}
(rendered, lines)
}
fn render_line_plain_block(block_src: &str, block_source_start: u32) -> (String, Vec<LineMapping>) {
let mut rendered = String::with_capacity(block_src.len());
let mut lines: Vec<LineMapping> = Vec::new();
let mut offset_in_block: u32 = 0;
for raw_line in block_src.split_inclusive('\n') {
let has_newline = raw_line.ends_with('\n');
let line_content = raw_line.strip_suffix('\n').unwrap_or(raw_line);
let line_content = line_content.strip_suffix('\r').unwrap_or(line_content);
let bytes = line_content.as_bytes();
let indent = bytes
.iter()
.take_while(|&&byte| byte == b' ' || byte == b'\t')
.count();
if !bytes[indent..].starts_with(b"//") {
offset_in_block += raw_line.len() as u32;
continue;
}
let mut content_start = indent + 2;
if content_start < bytes.len() && bytes[content_start] == b' ' {
content_start += 1;
}
let content = &line_content[content_start..];
let rendered_start = rendered.len();
rendered.push_str(content);
let line_source_offset = block_source_start + offset_in_block + content_start as u32;
lines.push(LineMapping {
rendered_start,
rendered_len: content.len(),
source_offset: line_source_offset,
});
if has_newline {
rendered.push('\n');
}
offset_in_block += raw_line.len() as u32;
}
(rendered, lines)
}
fn render_block_comment(
body_text: &str,
block_source_start: u32,
open: &str,
close: &str,
) -> (String, Vec<LineMapping>) {
let body = body_text
.strip_prefix(open)
.and_then(|inner| inner.strip_suffix(close))
.unwrap_or(body_text);
let prefix_len = open.len() as u32;
let mut rendered = String::with_capacity(body.len());
let mut lines: Vec<LineMapping> = Vec::new();
let mut offset_in_body: u32 = 0;
for raw_line in body.split_inclusive('\n') {
let has_newline = raw_line.ends_with('\n');
let line_content = raw_line.strip_suffix('\n').unwrap_or(raw_line);
let line_content = line_content.strip_suffix('\r').unwrap_or(line_content);
let bytes = line_content.as_bytes();
let mut content_start: usize = 0;
while content_start < bytes.len()
&& (bytes[content_start] == b' ' || bytes[content_start] == b'\t')
{
content_start += 1;
}
if content_start < bytes.len() && bytes[content_start] == b'*' {
content_start += 1;
if content_start < bytes.len() && bytes[content_start] == b' ' {
content_start += 1;
}
} else {
content_start = 0;
}
let content = &line_content[content_start..];
let rendered_start = rendered.len();
rendered.push_str(content);
let line_source_offset =
block_source_start + prefix_len + offset_in_body + content_start as u32;
lines.push(LineMapping {
rendered_start,
rendered_len: content.len(),
source_offset: line_source_offset,
});
if has_newline {
rendered.push('\n');
}
offset_in_body += raw_line.len() as u32;
}
(rendered, lines)
}