wat_service 0.7.0

WebAssembly Text Format language service.
Documentation
use crate::{
    LanguageService, LintLevel,
    binder::{Symbol, SymbolKind, SymbolTable},
    helpers,
};
use line_index::LineIndex;
use lspt::{Diagnostic, DiagnosticSeverity, DiagnosticTag, Union2};
use rowan::{Direction, TextRange, ast::support};
use wat_syntax::{SyntaxKind, SyntaxNode};

const DIAGNOSTIC_CODE: &str = "unused";

pub fn check(
    service: &LanguageService,
    diagnostics: &mut Vec<Diagnostic>,
    lint_level: LintLevel,
    line_index: &LineIndex,
    root: &SyntaxNode,
    symbol_table: &SymbolTable,
) {
    let severity = match lint_level {
        LintLevel::Allow => return,
        LintLevel::Hint => DiagnosticSeverity::Hint,
        LintLevel::Warn => DiagnosticSeverity::Warning,
        LintLevel::Deny => DiagnosticSeverity::Error,
    };
    diagnostics.extend(symbol_table.symbols.values().filter_map(|symbol| {
        match symbol.kind {
            SymbolKind::Func
            | SymbolKind::Type
            | SymbolKind::GlobalDef
            | SymbolKind::MemoryDef
            | SymbolKind::TableDef
            | SymbolKind::TagDef => {
                if is_prefixed_with_underscore(service, symbol)
                    || is_used(symbol_table, symbol)
                    || is_exported(root, symbol)
                {
                    None
                } else {
                    Some(report(service, line_index, root, severity, symbol))
                }
            }
            SymbolKind::Param | SymbolKind::Local => {
                if is_prefixed_with_underscore(service, symbol)
                    || is_used(symbol_table, symbol)
                    || symbol
                        .key
                        .to_node(root)
                        .parent()
                        .and_then(|parent| {
                            if parent.kind() == SyntaxKind::TYPE_USE {
                                Some(parent)
                            } else {
                                parent.parent()
                            }
                        })
                        .map(|node| {
                            node.siblings(Direction::Prev)
                                .any(|sibling| sibling.kind() == SyntaxKind::IMPORT)
                        })
                        .unwrap_or_default()
                {
                    None
                } else {
                    let node = symbol.key.to_node(root);
                    let range = support::token(&node, SyntaxKind::IDENT)
                        .map(|token| token.text_range())
                        .unwrap_or_else(|| node.text_range());
                    Some(report_with_range(
                        service, line_index, range, severity, symbol,
                    ))
                }
            }
            SymbolKind::FieldDef => {
                if is_prefixed_with_underscore(service, symbol) || is_used(symbol_table, symbol) {
                    None
                } else {
                    let node = symbol.key.to_node(root);
                    let range = support::token(&node, SyntaxKind::IDENT)
                        .map(|token| token.text_range())
                        .unwrap_or_else(|| node.text_range());
                    Some(report_with_range(
                        service, line_index, range, severity, symbol,
                    ))
                }
            }
            _ => None,
        }
    }));
}

fn is_prefixed_with_underscore(service: &LanguageService, symbol: &Symbol) -> bool {
    symbol
        .idx
        .name
        .is_some_and(|name| name.ident(service).starts_with("$_"))
}

fn is_used(symbol_table: &SymbolTable, symbol: &Symbol) -> bool {
    symbol_table.resolved.values().any(|key| key == &symbol.key)
}

fn is_exported(root: &SyntaxNode, def_symbol: &Symbol) -> bool {
    let node = def_symbol.key.to_node(root);
    node.children()
        .any(|child| child.kind() == SyntaxKind::EXPORT)
}

fn report(
    service: &LanguageService,
    line_index: &LineIndex,
    root: &SyntaxNode,
    severity: DiagnosticSeverity,
    symbol: &Symbol,
) -> Diagnostic {
    let node = symbol.key.to_node(root);
    let range = support::token(&node, SyntaxKind::IDENT)
        .or_else(|| support::token(&node, SyntaxKind::KEYWORD))
        .map(|token| token.text_range())
        .unwrap_or_else(|| node.text_range());
    report_with_range(service, line_index, range, severity, symbol)
}

fn report_with_range(
    service: &LanguageService,
    line_index: &LineIndex,
    range: TextRange,
    severity: DiagnosticSeverity,
    symbol: &Symbol,
) -> Diagnostic {
    Diagnostic {
        range: helpers::rowan_range_to_lsp_range(line_index, range),
        severity: Some(severity),
        source: Some("wat".into()),
        code: Some(Union2::B(DIAGNOSTIC_CODE.into())),
        message: format!(
            "{} `{}` is never used",
            symbol.kind,
            symbol.idx.render(service),
        ),
        tags: Some(vec![DiagnosticTag::Unnecessary]),
        ..Default::default()
    }
}