use crate::inline_config::{InlineConfigItem, InvalidInlineConfigItem};
use itertools::Itertools;
use solang_parser::pt::*;
use std::collections::VecDeque;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CommentType {
Line,
Block,
DocLine,
DocBlock,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CommentPosition {
Prefix,
Postfix,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CommentWithMetadata {
pub ty: CommentType,
pub loc: Loc,
pub has_newline_before: bool,
pub indent_len: usize,
pub comment: String,
pub position: CommentPosition,
}
impl PartialOrd for CommentWithMetadata {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for CommentWithMetadata {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.loc.cmp(&other.loc)
}
}
impl CommentWithMetadata {
fn new(
comment: Comment,
position: CommentPosition,
has_newline_before: bool,
indent_len: usize,
) -> Self {
let (ty, loc, comment) = match comment {
Comment::Line(loc, comment) => (CommentType::Line, loc, comment),
Comment::Block(loc, comment) => (CommentType::Block, loc, comment),
Comment::DocLine(loc, comment) => (CommentType::DocLine, loc, comment),
Comment::DocBlock(loc, comment) => (CommentType::DocBlock, loc, comment),
};
Self {
comment: comment.trim_end().to_string(),
ty,
loc,
position,
has_newline_before,
indent_len,
}
}
fn from_comment_and_src(comment: Comment, src: &str, last_comment: Option<&Self>) -> Self {
let src_before = &src[..comment.loc().start()];
if src_before.is_empty() {
return Self::new(comment, CommentPosition::Prefix, false, 0)
}
let mut lines_before = src_before.lines().rev();
let this_line =
if src_before.ends_with('\n') { "" } else { lines_before.next().unwrap_or_default() };
let indent_len = this_line.chars().take_while(|c| c.is_whitespace()).count();
let last_line = lines_before.next().map(str::trim_start);
if matches!(comment, Comment::DocLine(..) | Comment::DocBlock(..)) {
return Self::new(
comment,
CommentPosition::Prefix,
last_line.map_or(true, str::is_empty),
indent_len,
)
}
let mut code_end = 0;
for (state, idx, ch) in src_before.comment_state_char_indices() {
if matches!(state, CommentState::None) && !ch.is_whitespace() {
code_end = idx;
}
}
let (position, has_newline_before) = if src_before[code_end..].contains('\n') {
if let Some(last_line) = last_line {
if last_line.is_empty() {
(CommentPosition::Prefix, true)
} else {
if last_comment
.is_some_and(|last| last.loc.end() > code_end && !last.is_prefix())
{
let next_indent_len = src[comment.loc().end()..]
.non_comment_chars()
.take_while(|ch| ch.is_whitespace())
.fold(indent_len, |indent, ch| if ch == '\n' { 0 } else { indent + 1 });
if indent_len > next_indent_len {
(CommentPosition::Postfix, false)
} else {
(CommentPosition::Prefix, false)
}
} else {
(CommentPosition::Prefix, false)
}
}
} else {
(CommentPosition::Prefix, false)
}
} else {
(CommentPosition::Postfix, false)
};
Self::new(comment, position, has_newline_before, indent_len)
}
pub fn is_line(&self) -> bool {
matches!(self.ty, CommentType::Line | CommentType::DocLine)
}
pub fn is_doc_block(&self) -> bool {
matches!(self.ty, CommentType::DocBlock)
}
pub fn is_prefix(&self) -> bool {
matches!(self.position, CommentPosition::Prefix)
}
pub fn is_before(&self, byte: usize) -> bool {
self.loc.start() < byte
}
pub fn contents(&self) -> &str {
let mut s = self.comment.as_str();
if let Some(stripped) = s.strip_prefix(self.start_token()) {
s = stripped;
}
if let Some(end_token) = self.end_token() {
if let Some(stripped) = s.strip_suffix(end_token) {
s = stripped;
}
}
s
}
#[inline]
pub const fn start_token(&self) -> &'static str {
match self.ty {
CommentType::Line => "//",
CommentType::Block => "/*",
CommentType::DocLine => "///",
CommentType::DocBlock => "/**",
}
}
#[inline]
pub const fn wrap_token(&self) -> &'static str {
match self.ty {
CommentType::Line => "// ",
CommentType::DocLine => "/// ",
CommentType::Block => "",
CommentType::DocBlock => " * ",
}
}
#[inline]
pub const fn end_token(&self) -> Option<&'static str> {
match self.ty {
CommentType::Line | CommentType::DocLine => None,
CommentType::Block | CommentType::DocBlock => Some("*/"),
}
}
}
#[derive(Clone, Debug, Default)]
pub struct Comments {
prefixes: VecDeque<CommentWithMetadata>,
postfixes: VecDeque<CommentWithMetadata>,
}
impl Comments {
pub fn new(mut comments: Vec<Comment>, src: &str) -> Self {
let mut prefixes = VecDeque::with_capacity(comments.len());
let mut postfixes = VecDeque::with_capacity(comments.len());
let mut last_comment = None;
comments.sort_by_key(|comment| comment.loc());
for comment in comments {
let comment = CommentWithMetadata::from_comment_and_src(comment, src, last_comment);
let vec = if comment.is_prefix() { &mut prefixes } else { &mut postfixes };
vec.push_back(comment);
last_comment = Some(vec.back().unwrap());
}
Self { prefixes, postfixes }
}
fn remove_comments_before(
comments: &mut VecDeque<CommentWithMetadata>,
byte: usize,
) -> Vec<CommentWithMetadata> {
let pos = comments
.iter()
.find_position(|comment| !comment.is_before(byte))
.map(|(idx, _)| idx)
.unwrap_or_else(|| comments.len());
if pos == 0 {
return Vec::new()
}
comments.rotate_left(pos);
comments.split_off(comments.len() - pos).into()
}
pub(crate) fn remove_prefixes_before(&mut self, byte: usize) -> Vec<CommentWithMetadata> {
Self::remove_comments_before(&mut self.prefixes, byte)
}
pub(crate) fn remove_postfixes_before(&mut self, byte: usize) -> Vec<CommentWithMetadata> {
Self::remove_comments_before(&mut self.postfixes, byte)
}
pub(crate) fn remove_all_comments_before(&mut self, byte: usize) -> Vec<CommentWithMetadata> {
self.remove_prefixes_before(byte)
.into_iter()
.merge(self.remove_postfixes_before(byte))
.collect()
}
pub(crate) fn pop(&mut self) -> Option<CommentWithMetadata> {
if self.iter().next()?.is_prefix() {
self.prefixes.pop_front()
} else {
self.postfixes.pop_front()
}
}
pub(crate) fn iter(&self) -> impl Iterator<Item = &CommentWithMetadata> {
self.prefixes.iter().merge(self.postfixes.iter())
}
pub fn parse_inline_config_items(
&self,
) -> impl Iterator<Item = Result<(Loc, InlineConfigItem), (Loc, InvalidInlineConfigItem)>> + '_
{
self.iter()
.filter_map(|comment| {
Some((comment, comment.contents().trim_start().strip_prefix("forgefmt:")?.trim()))
})
.map(|(comment, item)| {
let loc = comment.loc;
item.parse().map(|out| (loc, out)).map_err(|out| (loc, out))
})
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum CommentState {
#[default]
None,
LineStart1,
LineStart2,
Line,
BlockStart1,
BlockStart2,
Block,
BlockEnd1,
BlockEnd2,
}
pub struct CommentStateCharIndices<'a> {
iter: std::str::CharIndices<'a>,
state: CommentState,
}
impl<'a> CommentStateCharIndices<'a> {
#[inline]
fn new(string: &'a str) -> Self {
Self { iter: string.char_indices(), state: CommentState::None }
}
#[inline]
pub fn with_state(mut self, state: CommentState) -> Self {
self.state = state;
self
}
#[inline]
pub fn peek(&mut self) -> Option<(usize, char)> {
self.iter.clone().next()
}
}
impl Iterator for CommentStateCharIndices<'_> {
type Item = (CommentState, usize, char);
#[inline]
fn next(&mut self) -> Option<Self::Item> {
let (idx, ch) = self.iter.next()?;
match self.state {
CommentState::None => {
if ch == '/' {
self.state = match self.peek() {
Some((_, '/')) => CommentState::LineStart1,
Some((_, '*')) => CommentState::BlockStart1,
_ => CommentState::None,
};
}
}
CommentState::LineStart1 => {
self.state = CommentState::LineStart2;
}
CommentState::LineStart2 => {
self.state = CommentState::Line;
}
CommentState::Line => {
if ch == '\n' {
self.state = CommentState::None;
}
}
CommentState::BlockStart1 => {
self.state = CommentState::BlockStart2;
}
CommentState::BlockStart2 => {
self.state = CommentState::Block;
}
CommentState::Block => {
if ch == '*' {
if let Some((_, '/')) = self.peek() {
self.state = CommentState::BlockEnd1;
}
}
}
CommentState::BlockEnd1 => {
self.state = CommentState::BlockEnd2;
}
CommentState::BlockEnd2 => {
self.state = CommentState::None;
}
}
Some((self.state, idx, ch))
}
#[inline]
fn size_hint(&self) -> (usize, Option<usize>) {
self.iter.size_hint()
}
#[inline]
fn count(self) -> usize {
self.iter.count()
}
}
impl std::iter::FusedIterator for CommentStateCharIndices<'_> {}
pub struct NonCommentChars<'a>(CommentStateCharIndices<'a>);
impl Iterator for NonCommentChars<'_> {
type Item = char;
#[inline]
fn next(&mut self) -> Option<Self::Item> {
for (state, _, ch) in self.0.by_ref() {
if state == CommentState::None {
return Some(ch)
}
}
None
}
}
pub trait CommentStringExt {
fn comment_state_char_indices(&self) -> CommentStateCharIndices<'_>;
#[inline]
fn non_comment_chars(&self) -> NonCommentChars<'_> {
NonCommentChars(self.comment_state_char_indices())
}
#[inline]
fn trim_comments(&self) -> String {
self.non_comment_chars().collect()
}
}
impl<T> CommentStringExt for T
where
T: AsRef<str>,
{
#[inline]
fn comment_state_char_indices(&self) -> CommentStateCharIndices<'_> {
CommentStateCharIndices::new(self.as_ref())
}
}
impl CommentStringExt for str {
#[inline]
fn comment_state_char_indices(&self) -> CommentStateCharIndices<'_> {
CommentStateCharIndices::new(self)
}
}