rusty-javac 0.2.3

A Java compiler written in Rust.
Documentation
use crate::ast::{JavaSyntaxKind, JavaSyntaxNode, JavaSyntaxToken};
use crate::hir::lowering::{LowerError, LowerResult};
use text_size::TextRange;

#[derive(Debug, Clone)]
pub(super) struct ExprToken {
    pub kind: JavaSyntaxKind,
    pub text: String,
    pub line: u16,
    pub range: TextRange,
}

impl From<JavaSyntaxToken> for ExprToken {
    fn from(token: JavaSyntaxToken) -> Self {
        let line = token_source_line(&token);
        let range = token.text_range();
        Self {
            kind: token.kind(),
            text: token.text().to_string(),
            line,
            range,
        }
    }
}

pub(super) fn first_ident(node: &JavaSyntaxNode) -> Option<JavaSyntaxToken> {
    node.children_with_tokens()
        .filter_map(|element| element.into_token())
        .find(|token| token.kind() == JavaSyntaxKind::Ident)
}

pub(super) fn source_line(node: &JavaSyntaxNode) -> u16 {
    let root = node.ancestors().last().unwrap_or_else(|| node.clone());
    let start = node
        .descendants_with_tokens()
        .filter_map(|element| element.into_token())
        .find(|token| {
            !matches!(
                token.kind(),
                JavaSyntaxKind::Whitespace | JavaSyntaxKind::Comment
            )
        })
        .map(|token| token.text_range().start())
        .unwrap_or_else(|| node.text_range().start());
    let start = u32::from(start) as usize;
    source_line_at(&root, start)
}

pub(super) fn token_source_line(token: &JavaSyntaxToken) -> u16 {
    let Some(parent) = token.parent() else {
        return 1;
    };
    let root = parent.ancestors().last().unwrap_or(parent);
    let start = u32::from(token.text_range().start()) as usize;
    source_line_at(&root, start)
}

fn source_line_at(root: &JavaSyntaxNode, start: usize) -> u16 {
    let text = root.text().to_string();
    let offset = start.min(text.len());
    let line = text.as_bytes()[..offset]
        .iter()
        .filter(|byte| **byte == b'\n')
        .count()
        + 1;
    line.min(u16::MAX as usize) as u16
}

pub(super) fn last_ident(node: &JavaSyntaxNode) -> Option<JavaSyntaxToken> {
    node.children_with_tokens()
        .filter_map(|element| element.into_token())
        .filter(|token| token.kind() == JavaSyntaxKind::Ident)
        .last()
}

pub(super) fn initializer_tokens(node: &JavaSyntaxNode) -> Option<Vec<ExprToken>> {
    let mut seen_eq = false;
    let tokens = node
        .descendants_with_tokens()
        .filter_map(|element| element.into_token())
        .filter_map(|token| {
            if !seen_eq {
                seen_eq = token.kind() == JavaSyntaxKind::Eq;
                return None;
            }
            if seen_eq && is_expr_token(&token) {
                Some(ExprToken::from(token))
            } else {
                None
            }
        })
        .collect::<Vec<_>>();

    if tokens.is_empty() {
        None
    } else {
        Some(tokens)
    }
}

pub(super) fn expr_tokens(node: &JavaSyntaxNode) -> Vec<ExprToken> {
    node.descendants_with_tokens()
        .filter_map(|element| element.into_token())
        .filter(is_expr_token)
        .map(ExprToken::from)
        .collect()
}

pub(super) fn tokens_in_first_parens(node: &JavaSyntaxNode) -> LowerResult<Vec<ExprToken>> {
    let mut seen_open = false;
    let mut depth = 0usize;
    let mut tokens = Vec::new();

    for token in node
        .descendants_with_tokens()
        .filter_map(|element| element.into_token())
    {
        match token.kind() {
            JavaSyntaxKind::LParen => {
                seen_open = true;
                depth += 1;
                if depth > 1 && is_expr_token(&token) {
                    tokens.push(ExprToken::from(token));
                }
            }
            JavaSyntaxKind::RParen if seen_open => {
                depth = depth.saturating_sub(1);
                if depth == 0 {
                    return Ok(tokens);
                }
                if is_expr_token(&token) {
                    tokens.push(ExprToken::from(token));
                }
            }
            _ if seen_open && depth > 0 && is_expr_token(&token) => {
                tokens.push(ExprToken::from(token));
            }
            _ => {}
        }
    }

    Err(LowerError::UnsupportedExpression)
}

pub(super) fn tokens_after_keyword(
    node: &JavaSyntaxNode,
    keyword: JavaSyntaxKind,
) -> Vec<ExprToken> {
    let mut seen_keyword = false;
    let mut tokens = Vec::new();

    for token in node
        .descendants_with_tokens()
        .filter_map(|element| element.into_token())
    {
        if token.kind() == keyword {
            seen_keyword = true;
            continue;
        }
        if seen_keyword {
            if token.kind() == JavaSyntaxKind::Semi && !is_inside_anonymous_class_body(&token) {
                break;
            }
            if is_expr_token(&token) {
                tokens.push(ExprToken::from(token));
            }
        }
    }

    tokens
}

pub(super) fn case_pattern_tokens(label: &JavaSyntaxNode) -> Vec<ExprToken> {
    let mut in_pattern = false;
    let mut tokens = Vec::new();

    for token in label
        .descendants_with_tokens()
        .filter_map(|element| element.into_token())
    {
        match token.kind() {
            JavaSyntaxKind::CaseKw => in_pattern = true,
            JavaSyntaxKind::Arrow | JavaSyntaxKind::Colon if in_pattern => break,
            _ if in_pattern && is_expr_token(&token) => tokens.push(ExprToken::from(token)),
            _ => {}
        }
    }

    tokens
}

pub(super) fn qualified_name_text(node: &JavaSyntaxNode) -> LowerResult<String> {
    let (text, _) = qualified_name_text_range(node)?;
    Ok(text)
}

pub(super) fn qualified_name_text_range(node: &JavaSyntaxNode) -> LowerResult<(String, TextRange)> {
    let Some(name) = node
        .descendants()
        .find(|child| child.kind() == JavaSyntaxKind::QualifiedName)
    else {
        return Err(LowerError::MissingImportName);
    };

    let text = name
        .children_with_tokens()
        .filter_map(|element| element.into_token())
        .filter(|token| matches!(token.kind(), JavaSyntaxKind::Ident | JavaSyntaxKind::Dot))
        .map(|token| token.text().to_string())
        .collect::<String>();

    if text.is_empty() {
        Err(LowerError::MissingImportName)
    } else {
        Ok((text, name.text_range()))
    }
}

fn is_expr_token(token: &JavaSyntaxToken) -> bool {
    if matches!(
        token.kind(),
        JavaSyntaxKind::Whitespace | JavaSyntaxKind::Comment
    ) {
        return false;
    }
    token.kind() != JavaSyntaxKind::Semi || is_inside_anonymous_class_body(token)
}

fn is_inside_anonymous_class_body(token: &JavaSyntaxToken) -> bool {
    token.parent().is_some_and(|parent| {
        parent
            .ancestors()
            .filter(|node| node.kind() == JavaSyntaxKind::ClassBody)
            .any(|body| {
                body.ancestors()
                    .any(|ancestor| ancestor.kind() == JavaSyntaxKind::NewExpr)
            })
    })
}