wat_service 0.2.0

WebAssembly Text Format language service.
Documentation
use crate::{
    binder::{SymbolItem, SymbolItemKind, SymbolTable},
    helpers,
    idx::IdentsCtx,
    InternUri, LanguageService, LintLevel,
};
use line_index::LineIndex;
use lsp_types::{Diagnostic, DiagnosticSeverity, DiagnosticTag, NumberOrString};
use rowan::ast::support;
use wat_syntax::{SyntaxKind, SyntaxNode};

const DIAGNOSTIC_CODE: &str = "unused";

pub fn check(
    service: &LanguageService,
    diags: &mut Vec<Diagnostic>,
    uri: InternUri,
    line_index: &LineIndex,
    root: &SyntaxNode,
    symbol_table: &SymbolTable,
) {
    let severity = match service.get_config(uri).lint.unused {
        LintLevel::Allow => return,
        LintLevel::Warn => DiagnosticSeverity::WARNING,
        LintLevel::Deny => DiagnosticSeverity::ERROR,
    };
    diags.extend(
        symbol_table
            .symbols
            .iter()
            .filter_map(|symbol| match symbol.kind {
                SymbolItemKind::Func => {
                    if !is_used(symbol_table, symbol, SymbolItemKind::Call)
                        && !is_exported(root, symbol)
                    {
                        Some(report(service, line_index, root, severity, symbol))
                    } else {
                        None
                    }
                }
                SymbolItemKind::Param | SymbolItemKind::Local => {
                    if !is_used(symbol_table, symbol, SymbolItemKind::LocalRef) {
                        Some(report(service, line_index, root, severity, symbol))
                    } else {
                        None
                    }
                }
                SymbolItemKind::Type => {
                    if !is_used(symbol_table, symbol, SymbolItemKind::TypeUse)
                        && !is_exported(root, symbol)
                    {
                        Some(report(service, line_index, root, severity, symbol))
                    } else {
                        None
                    }
                }
                SymbolItemKind::GlobalDef => {
                    if !is_used(symbol_table, symbol, SymbolItemKind::GlobalRef)
                        && !is_exported(root, symbol)
                    {
                        Some(report(service, line_index, root, severity, symbol))
                    } else {
                        None
                    }
                }
                SymbolItemKind::MemoryDef => {
                    if !is_used(symbol_table, symbol, SymbolItemKind::MemoryRef)
                        && !is_exported(root, symbol)
                    {
                        Some(report(service, line_index, root, severity, symbol))
                    } else {
                        None
                    }
                }
                SymbolItemKind::TableDef => {
                    if !is_used(symbol_table, symbol, SymbolItemKind::TableRef)
                        && !is_exported(root, symbol)
                    {
                        Some(report(service, line_index, root, severity, symbol))
                    } else {
                        None
                    }
                }
                _ => None,
            }),
    );
}

fn is_used(symbol_table: &SymbolTable, def_symbol: &SymbolItem, ref_kind: SymbolItemKind) -> bool {
    symbol_table.symbols.iter().any(|other| {
        other.kind == ref_kind
            && other.idx.is_defined_by(&def_symbol.idx)
            && other.region == def_symbol.region
    })
}

fn is_exported(root: &SyntaxNode, def_symbol: &SymbolItem) -> 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: &SymbolItem,
) -> 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());
    Diagnostic {
        range: helpers::rowan_range_to_lsp_range(line_index, range),
        severity: Some(severity),
        source: Some("wat".into()),
        code: Some(NumberOrString::String(DIAGNOSTIC_CODE.into())),
        message: format!(
            "`{}` is never used",
            symbol
                .idx
                .name
                .map(|name| service.lookup_ident(name))
                .or_else(|| symbol.idx.num.map(|num| num.to_string()))
                .unwrap_or_default()
        ),
        tags: Some(vec![DiagnosticTag::UNNECESSARY]),
        ..Default::default()
    }
}