wat_service 0.7.0

WebAssembly Text Format language service.
Documentation
use crate::{
    LanguageService,
    binder::{SymbolKey, SymbolKind, SymbolTable},
    document::Document,
    helpers, types_analyzer,
};
use line_index::LineIndex;
use lspt::{Diagnostic, DiagnosticSeverity, Union2};
use rowan::{
    TextRange,
    ast::{AstNode, support},
};
use rustc_hash::FxHashMap;
use wat_syntax::{
    SyntaxKind, SyntaxNode,
    ast::{BlockInstr, Instr},
};

const DIAGNOSTIC_CODE: &str = "uninit";

pub fn check(
    service: &LanguageService,
    diagnostics: &mut Vec<Diagnostic>,
    document: Document,
    line_index: &LineIndex,
    symbol_table: &SymbolTable,
    node: &SyntaxNode,
) {
    let region = SymbolKey::new(node);
    let locals = symbol_table
        .symbols
        .values()
        .filter(|symbol| symbol.kind == SymbolKind::Local && symbol.region == region)
        .map(|symbol| {
            let ty = types_analyzer::extract_type(service, document, symbol.green.clone());
            (
                symbol.key,
                if let Some(false) = ty.map(|ty| ty.defaultable()) {
                    Init::Unset
                } else {
                    Init::Set
                },
            )
        })
        .collect::<FxHashMap<_, _>>();
    Checker {
        service,
        diagnostics,
        line_index,
        symbol_table,
        locals,
    }
    .check(node);
}

struct Checker<'a, 'db> {
    service: &'a LanguageService,
    diagnostics: &'a mut Vec<Diagnostic>,
    line_index: &'a LineIndex,
    symbol_table: &'a SymbolTable<'db>,
    locals: FxHashMap<SymbolKey, Init>,
}
impl Checker<'_, '_> {
    fn check(&mut self, node: &SyntaxNode) {
        let conditional = matches!(
            node.kind(),
            SyntaxKind::BLOCK_IF_THEN | SyntaxKind::BLOCK_IF_ELSE
        );
        support::children::<Instr>(node).for_each(|instr| match instr {
            Instr::Plain(plain_instr) => {
                self.check(plain_instr.syntax());

                let Some(token) = plain_instr.instr_name() else {
                    return;
                };
                match token.text() {
                    "local.get" => {
                        let Some(immediate) = plain_instr.immediates().next() else {
                            return;
                        };
                        let immediate = immediate.syntax();
                        let Some(def_symbol) =
                            self.symbol_table.find_def(SymbolKey::new(immediate))
                        else {
                            return;
                        };
                        let set = match self.locals.get(&def_symbol.key) {
                            Some(Init::Set) | None => true,
                            Some(Init::Conditional(range)) => {
                                range.contains_range(immediate.text_range())
                            }
                            Some(Init::Unset) => false,
                        };
                        if !set {
                            self.diagnostics.push(Diagnostic {
                                range: helpers::rowan_range_to_lsp_range(
                                    self.line_index,
                                    immediate.text_range(),
                                ),
                                severity: Some(DiagnosticSeverity::Error),
                                source: Some("wat".into()),
                                code: Some(Union2::B(DIAGNOSTIC_CODE.into())),
                                message: format!(
                                    "local `{}` is used before being initialized",
                                    def_symbol.idx.render(self.service)
                                ),
                                ..Default::default()
                            });
                        }
                    }
                    "local.set" | "local.tee" => {
                        if let Some(initialized) = plain_instr
                            .immediates()
                            .next()
                            .and_then(|immediate| {
                                self.symbol_table
                                    .resolved
                                    .get(&SymbolKey::new(immediate.syntax()))
                            })
                            .and_then(|key| self.locals.get_mut(key))
                        {
                            if conditional && matches!(initialized, Init::Unset) {
                                *initialized = Init::Conditional(node.text_range());
                            } else {
                                *initialized = Init::Set;
                            }
                        }
                    }
                    _ => {}
                }
            }
            Instr::Block(BlockInstr::Block(block_block)) => {
                self.check(block_block.syntax());
            }
            Instr::Block(BlockInstr::Loop(block_loop)) => {
                self.check(block_loop.syntax());
            }
            Instr::Block(BlockInstr::If(block_if)) => {
                self.check(block_if.syntax());
                if let Some(then_block) = block_if.then_block() {
                    self.check(then_block.syntax());
                }
                if let Some(else_block) = block_if.else_block() {
                    self.check(else_block.syntax());
                }
            }
            Instr::Block(BlockInstr::TryTable(block_try_table)) => {
                self.check(block_try_table.syntax());
            }
        });
    }
}

enum Init {
    Unset,
    Conditional(TextRange),
    Set,
}