tinymist_lint/
lib.rs

1//! A linter for Typst.
2
3mod rules;
4
5use std::{cell::OnceCell, sync::Arc};
6
7use tinymist_analysis::{
8    adt::interner::Interned,
9    syntax::{Decl, ExprInfo},
10    ty::{Ty, TyCtx, TypeInfo},
11};
12use tinymist_project::LspWorld;
13use typst::{
14    diag::{EcoString, SourceDiagnostic, Tracepoint, eco_format},
15    ecow::EcoVec,
16    syntax::{
17        FileId, Span, Spanned, SyntaxNode,
18        ast::{self, AstNode},
19    },
20};
21
22/// A type alias for a vector of diagnostics.
23type DiagnosticVec = EcoVec<SourceDiagnostic>;
24
25/// The lint information about a file.
26#[derive(Debug, Clone)]
27pub struct LintInfo {
28    /// The revision of expression information
29    pub revision: usize,
30    /// The belonging file id
31    pub fid: FileId,
32    /// The diagnostics
33    pub diagnostics: DiagnosticVec,
34}
35
36/// Performs linting check on file and returns a vector of diagnostics.
37pub fn lint_file(
38    world: &LspWorld,
39    ei: &ExprInfo,
40    ti: Arc<TypeInfo>,
41    known_issues: KnownIssues,
42) -> LintInfo {
43    let diagnostics = Linter::new(world, ei.clone(), ti, known_issues).lint(ei.source.root());
44    LintInfo {
45        revision: ei.revision,
46        fid: ei.fid,
47        diagnostics,
48    }
49}
50
51/// Information about issues the linter checks for that will already be reported
52/// to the user via other means (such as compiler diagnostics), to avoid
53/// duplicating warnings.
54#[derive(Default, Clone, Hash)]
55pub struct KnownIssues {
56    unknown_vars: EcoVec<Span>,
57    unknown_fonts: EcoVec<(Span, EcoString)>,
58}
59
60impl KnownIssues {
61    /// Collects known lint issues from the given compiler diagnostics.
62    pub fn from_compiler_diagnostics<'a>(
63        diags: impl Iterator<Item = &'a SourceDiagnostic>,
64    ) -> Self {
65        let mut unknown_vars = Vec::default();
66        let mut unknown_fonts = Vec::default();
67        for diag in diags {
68            if diag.message.starts_with("unknown variable") {
69                unknown_vars.push(diag.span);
70            } else if let Some(font_name) = rules::bad_font::extract_unknown_font(&diag.message) {
71                unknown_fonts.push((diag.span, font_name));
72            }
73        }
74        unknown_vars.sort_by_key(|span| span.into_raw());
75        unknown_fonts.sort_by_key(|(span, _)| span.into_raw());
76        let unknown_vars = EcoVec::from(unknown_vars);
77        let unknown_fonts = EcoVec::from(unknown_fonts);
78        Self {
79            unknown_vars,
80            unknown_fonts,
81        }
82    }
83
84    pub(crate) fn has_unknown_math_ident(&self, ident: ast::MathIdent<'_>) -> bool {
85        self.unknown_vars.contains(&ident.span())
86    }
87
88    pub(crate) fn get_unknown_font(&self, span: Span) -> Option<&EcoString> {
89        self.unknown_fonts
90            .binary_search_by_key(&span.into_raw(), |(s, _)| s.into_raw())
91            .ok()
92            .map(|i| &self.unknown_fonts[i].1)
93    }
94}
95
96struct Linter<'w> {
97    world: &'w LspWorld,
98    ei: ExprInfo,
99    ti: Arc<TypeInfo>,
100    known_issues: KnownIssues,
101    diag: DiagnosticVec,
102    loop_info: Option<LoopInfo>,
103    func_info: Option<FuncInfo>,
104
105    /// Cached available fonts (sorted)
106    available_fonts: OnceCell<Vec<&'w str>>,
107}
108
109impl<'w> Linter<'w> {
110    fn new(
111        world: &'w LspWorld,
112        ei: ExprInfo,
113        ti: Arc<TypeInfo>,
114        known_issues: KnownIssues,
115    ) -> Self {
116        Self {
117            world,
118            ei,
119            ti,
120            known_issues,
121            diag: EcoVec::new(),
122            loop_info: None,
123            func_info: None,
124
125            available_fonts: OnceCell::new(),
126        }
127    }
128
129    fn tctx(&self) -> &impl TyCtx {
130        self.ti.as_ref()
131    }
132
133    fn lint(mut self, node: &SyntaxNode) -> DiagnosticVec {
134        if let Some(markup) = node.cast::<ast::Markup>() {
135            self.exprs(markup.exprs());
136        } else if let Some(expr) = node.cast() {
137            self.expr(expr);
138        }
139
140        self.diag
141    }
142
143    fn with_loop_info<F>(&mut self, span: Span, f: F) -> Option<()>
144    where
145        F: FnOnce(&mut Self) -> Option<()>,
146    {
147        let old = self.loop_info.take();
148        self.loop_info = Some(LoopInfo {
149            span,
150            has_break: false,
151            has_continue: false,
152        });
153        f(self);
154        self.loop_info = old;
155        Some(())
156    }
157
158    fn with_func_info<F>(&mut self, span: Span, f: F) -> Option<()>
159    where
160        F: FnOnce(&mut Self) -> Option<()>,
161    {
162        let old = self.func_info.take();
163        self.func_info = Some(FuncInfo {
164            span,
165            is_contextual: false,
166            has_return: false,
167            has_return_value: false,
168            parent_loop: self.loop_info.clone(),
169        });
170        f(self);
171        self.loop_info = self.func_info.take().expect("func info").parent_loop;
172        self.func_info = old;
173        Some(())
174    }
175
176    fn late_func_return(&mut self, f: impl FnOnce(LateFuncLinter) -> Option<()>) -> Option<()> {
177        let func_info = self.func_info.as_ref().expect("func info").clone();
178        f(LateFuncLinter {
179            linter: self,
180            func_info,
181            return_block_info: None,
182            expr_context: ExprContext::Block,
183        })
184    }
185
186    fn bad_branch_stmt(&mut self, expr: &SyntaxNode, name: &str) -> Option<()> {
187        let parent_loop = self
188            .func_info
189            .as_ref()
190            .map(|info| (info.parent_loop.as_ref(), info));
191
192        let mut diag = SourceDiagnostic::warning(
193            expr.span(),
194            eco_format!("`{name}` statement in a non-loop context"),
195        );
196        if let Some((Some(loop_info), func_info)) = parent_loop {
197            diag.trace.push(Spanned::new(
198                Tracepoint::Show(EcoString::inline("loop")),
199                loop_info.span,
200            ));
201            diag.trace
202                .push(Spanned::new(Tracepoint::Call(None), func_info.span));
203        }
204        self.diag.push(diag);
205
206        Some(())
207    }
208
209    #[inline(always)]
210    fn buggy_block_expr(&mut self, expr: ast::Expr, loc: BuggyBlockLoc) -> Option<()> {
211        self.buggy_block(Block::from(expr)?, loc)
212    }
213
214    fn buggy_block(&mut self, block: Block, loc: BuggyBlockLoc) -> Option<()> {
215        if self.only_show(block) {
216            let mut first = true;
217            for set in block.iter() {
218                let msg = match set {
219                    ast::Expr::SetRule(..) => "This set statement doesn't take effect.",
220                    ast::Expr::ShowRule(..) => "This show statement doesn't take effect.",
221                    _ => continue,
222                };
223                let mut warning = SourceDiagnostic::warning(set.span(), msg);
224                if first {
225                    first = false;
226                    warning.hint(loc.hint(set));
227                }
228                self.diag.push(warning);
229            }
230
231            return None;
232        }
233
234        Some(())
235    }
236
237    fn only_show(&mut self, block: Block) -> bool {
238        let mut has_set = false;
239
240        for it in block.iter() {
241            if is_show_set(it) {
242                has_set = true;
243            } else if matches!(it, ast::Expr::LoopBreak(..) | ast::Expr::LoopContinue(..)) {
244                return has_set;
245            } else if !it.to_untyped().kind().is_trivia() {
246                return false;
247            }
248        }
249
250        has_set
251    }
252
253    fn check_type_compare(&mut self, expr: ast::Binary<'_>) {
254        let op = expr.op();
255        if is_compare_op(op) {
256            let lhs = expr.lhs();
257            let rhs = expr.rhs();
258
259            let mut lhs = self.expr_ty(lhs);
260            let mut rhs = self.expr_ty(rhs);
261
262            let other_is_str = lhs.is_str(self.tctx());
263            if other_is_str {
264                (lhs, rhs) = (rhs, lhs);
265            }
266
267            if lhs.is_type(self.tctx()) && (other_is_str || rhs.is_str(self.tctx())) {
268                let msg = "comparing strings with types is deprecated";
269                let diag = SourceDiagnostic::warning(expr.span(), msg);
270                let diag = diag.with_hints([
271                    "compare with the literal type instead".into(),
272                    "this comparison will always return `false` since typst v0.14".into(),
273                ]);
274                self.diag.push(diag);
275            }
276        }
277    }
278
279    fn expr_ty<'a>(&self, expr: ast::Expr<'a>) -> TypedExpr<'a> {
280        TypedExpr {
281            expr,
282            ty: self.ti.type_of_span(expr.span()),
283        }
284    }
285}
286
287impl DataFlowVisitor for Linter<'_> {
288    fn exprs<'a>(&mut self, exprs: impl DoubleEndedIterator<Item = ast::Expr<'a>>) -> Option<()> {
289        for expr in exprs {
290            self.expr(expr);
291        }
292        Some(())
293    }
294
295    fn set(&mut self, expr: ast::SetRule<'_>) -> Option<()> {
296        if let Some(target) = expr.condition() {
297            self.expr(target);
298        }
299        self.exprs(expr.args().to_untyped().exprs());
300
301        if expr.target().to_untyped().text() == "text" {
302            self.check_bad_font(expr.args().items());
303        }
304
305        self.expr(expr.target())
306    }
307
308    fn show(&mut self, expr: ast::ShowRule<'_>) -> Option<()> {
309        if let Some(target) = expr.selector() {
310            self.expr(target);
311        }
312        let transform = expr.transform();
313        self.buggy_block_expr(transform, BuggyBlockLoc::Show(expr));
314        self.expr(transform)
315    }
316
317    fn conditional(&mut self, expr: ast::Conditional<'_>) -> Option<()> {
318        self.expr(expr.condition());
319
320        let if_body = expr.if_body();
321        self.buggy_block_expr(if_body, BuggyBlockLoc::IfTrue(expr));
322        self.expr(if_body);
323
324        if let Some(else_body) = expr.else_body() {
325            self.buggy_block_expr(else_body, BuggyBlockLoc::IfFalse(expr));
326            self.expr(else_body);
327        }
328
329        Some(())
330    }
331
332    fn while_loop(&mut self, expr: ast::WhileLoop<'_>) -> Option<()> {
333        self.with_loop_info(expr.span(), |this| {
334            this.expr(expr.condition());
335            let body = expr.body();
336            this.buggy_block_expr(body, BuggyBlockLoc::While(expr));
337            this.expr(body)
338        })
339    }
340
341    fn for_loop(&mut self, expr: ast::ForLoop<'_>) -> Option<()> {
342        self.with_loop_info(expr.span(), |this| {
343            this.expr(expr.iterable());
344            let body = expr.body();
345            this.buggy_block_expr(body, BuggyBlockLoc::For(expr));
346            this.expr(body)
347        })
348    }
349
350    fn contextual(&mut self, expr: ast::Contextual<'_>) -> Option<()> {
351        self.with_func_info(expr.span(), |this| {
352            this.loop_info = None;
353            this.func_info
354                .as_mut()
355                .expect("contextual function info")
356                .is_contextual = true;
357            this.expr(expr.body());
358            this.late_func_return(|mut this| this.late_contextual(expr))
359        })
360    }
361
362    fn closure(&mut self, expr: ast::Closure<'_>) -> Option<()> {
363        self.with_func_info(expr.span(), |this| {
364            this.loop_info = None;
365            this.exprs(expr.params().to_untyped().exprs());
366            this.expr(expr.body());
367            this.late_func_return(|mut this| this.late_closure(expr))
368        })
369    }
370
371    fn loop_break(&mut self, expr: ast::LoopBreak<'_>) -> Option<()> {
372        if let Some(info) = &mut self.loop_info {
373            info.has_break = true;
374        } else {
375            self.bad_branch_stmt(expr.to_untyped(), "break");
376        }
377        Some(())
378    }
379
380    fn loop_continue(&mut self, expr: ast::LoopContinue<'_>) -> Option<()> {
381        if let Some(info) = &mut self.loop_info {
382            info.has_continue = true;
383        } else {
384            self.bad_branch_stmt(expr.to_untyped(), "continue");
385        }
386        Some(())
387    }
388
389    fn func_return(&mut self, expr: ast::FuncReturn<'_>) -> Option<()> {
390        if let Some(info) = &mut self.func_info {
391            info.has_return = true;
392            info.has_return_value = expr.body().is_some();
393        } else {
394            self.diag.push(SourceDiagnostic::warning(
395                expr.span(),
396                "`return` statement in a non-function context",
397            ));
398        }
399        Some(())
400    }
401
402    fn binary(&mut self, expr: ast::Binary<'_>) -> Option<()> {
403        self.check_type_compare(expr);
404        self.exprs([expr.lhs(), expr.rhs()].into_iter())
405    }
406
407    fn func_call(&mut self, expr: ast::FuncCall<'_>) -> Option<()> {
408        // warn if text(font: ("Font Name", "Font Name")) in which Font Name ends with
409        // "VF"
410        if expr.callee().to_untyped().text() == "text" {
411            self.check_bad_font(expr.args().items());
412        }
413        self.exprs(expr.args().to_untyped().exprs().chain(expr.callee().once()));
414        Some(())
415    }
416
417    fn math_ident(&mut self, ident: ast::MathIdent<'_>) -> Option<()> {
418        let resolved = self.ei.get_def(&Interned::new(Decl::math_ident_ref(ident)));
419        let is_defined = resolved.is_some_and(|expr| expr.is_defined());
420
421        if !is_defined && !self.known_issues.has_unknown_math_ident(ident) {
422            let var = ident.as_str();
423            let mut warning =
424                SourceDiagnostic::warning(ident.span(), eco_format!("unknown variable: {var}"));
425
426            // Tries to produce the same hints as the corresponding Typst compiler error.
427            // See `unknown_variable_math` in typst-library/src/foundations/scope.rs:
428            // https://github.com/typst/typst/blob/v0.13.1/crates/typst-library/src/foundations/scope.rs#L386
429            let in_global = self.world.library.global.scope().get(var).is_some();
430            hint_unknown_variable_math(var, in_global, &mut warning);
431            self.diag.push(warning);
432        }
433
434        Some(())
435    }
436}
437
438struct LateFuncLinter<'a, 'b> {
439    linter: &'a mut Linter<'b>,
440    func_info: FuncInfo,
441    return_block_info: Option<ReturnBlockInfo>,
442    expr_context: ExprContext,
443}
444
445impl LateFuncLinter<'_, '_> {
446    fn late_closure(&mut self, expr: ast::Closure<'_>) -> Option<()> {
447        if !self.func_info.has_return {
448            return Some(());
449        }
450        self.expr(expr.body())
451    }
452
453    fn late_contextual(&mut self, expr: ast::Contextual<'_>) -> Option<()> {
454        if !self.func_info.has_return {
455            return Some(());
456        }
457        self.expr(expr.body())
458    }
459
460    fn expr_ctx<F>(&mut self, ctx: ExprContext, f: F) -> Option<()>
461    where
462        F: FnOnce(&mut Self) -> Option<()>,
463    {
464        let ctx = match ctx {
465            ExprContext::Block if self.expr_context != ExprContext::Block => ExprContext::BlockExpr,
466            a => a,
467        };
468        let old = std::mem::replace(&mut self.expr_context, ctx);
469        f(self);
470        self.expr_context = old;
471        Some(())
472    }
473
474    fn join(&mut self, parent: Option<ReturnBlockInfo>) {
475        if let Some(parent) = parent {
476            match &mut self.return_block_info {
477                Some(info) => {
478                    if info.return_value == parent.return_value {
479                        return;
480                    }
481
482                    // Merge the two return block info
483                    *info = parent.merge(std::mem::take(info));
484                }
485                info @ None => {
486                    *info = Some(parent);
487                }
488            }
489        }
490    }
491}
492
493impl DataFlowVisitor for LateFuncLinter<'_, '_> {
494    fn exprs<'a>(&mut self, exprs: impl DoubleEndedIterator<Item = ast::Expr<'a>>) -> Option<()> {
495        for expr in exprs.rev() {
496            self.expr(expr);
497        }
498        Some(())
499    }
500
501    fn block<'a>(&mut self, exprs: impl DoubleEndedIterator<Item = ast::Expr<'a>>) -> Option<()> {
502        self.expr_ctx(ExprContext::Block, |this| this.exprs(exprs))
503    }
504
505    fn loop_break(&mut self, _expr: ast::LoopBreak<'_>) -> Option<()> {
506        self.return_block_info = Some(ReturnBlockInfo {
507            return_value: false,
508            return_none: false,
509            warned: false,
510        });
511        Some(())
512    }
513
514    fn loop_continue(&mut self, _expr: ast::LoopContinue<'_>) -> Option<()> {
515        self.return_block_info = Some(ReturnBlockInfo {
516            return_value: false,
517            return_none: false,
518            warned: false,
519        });
520        Some(())
521    }
522
523    fn func_return(&mut self, expr: ast::FuncReturn<'_>) -> Option<()> {
524        if expr.body().is_some() {
525            self.return_block_info = Some(ReturnBlockInfo {
526                return_value: true,
527                return_none: false,
528                warned: false,
529            });
530        } else {
531            self.return_block_info = Some(ReturnBlockInfo {
532                return_value: false,
533                return_none: true,
534                warned: false,
535            });
536        }
537        Some(())
538    }
539
540    fn closure(&mut self, expr: ast::Closure<'_>) -> Option<()> {
541        let ident = expr.name().map(ast::Expr::Ident).into_iter();
542        let params = expr.params().to_untyped().exprs();
543        // the body is ignored in the return stmt analysis
544        let _body = expr.body().once();
545        self.exprs(ident.chain(params))
546    }
547
548    fn contextual(&mut self, expr: ast::Contextual<'_>) -> Option<()> {
549        // the body is ignored in the return stmt analysis
550        let _body = expr.body();
551        Some(())
552    }
553
554    fn field_access(&mut self, _expr: ast::FieldAccess<'_>) -> Option<()> {
555        Some(())
556    }
557
558    fn unary(&mut self, expr: ast::Unary<'_>) -> Option<()> {
559        self.expr_ctx(ExprContext::Expr, |this| this.expr(expr.expr()))
560    }
561
562    fn binary(&mut self, expr: ast::Binary<'_>) -> Option<()> {
563        self.expr_ctx(ExprContext::Expr, |this| {
564            this.exprs([expr.lhs(), expr.rhs()].into_iter())
565        })
566    }
567
568    fn equation(&mut self, expr: ast::Equation<'_>) -> Option<()> {
569        self.value(ast::Expr::Equation(expr));
570        Some(())
571    }
572
573    fn array(&mut self, expr: ast::Array<'_>) -> Option<()> {
574        self.value(ast::Expr::Array(expr));
575        Some(())
576    }
577
578    fn dict(&mut self, expr: ast::Dict<'_>) -> Option<()> {
579        self.value(ast::Expr::Dict(expr));
580        Some(())
581    }
582
583    fn include(&mut self, expr: ast::ModuleInclude<'_>) -> Option<()> {
584        self.value(ast::Expr::ModuleInclude(expr));
585        Some(())
586    }
587
588    fn func_call(&mut self, _expr: ast::FuncCall<'_>) -> Option<()> {
589        Some(())
590    }
591
592    fn let_binding(&mut self, _expr: ast::LetBinding<'_>) -> Option<()> {
593        Some(())
594    }
595
596    fn destruct_assign(&mut self, _expr: ast::DestructAssignment<'_>) -> Option<()> {
597        Some(())
598    }
599
600    fn conditional(&mut self, expr: ast::Conditional<'_>) -> Option<()> {
601        let if_body = expr.if_body();
602        let else_body = expr.else_body();
603
604        let parent = self.return_block_info.clone();
605        self.exprs(if_body.once());
606        let if_branch = std::mem::replace(&mut self.return_block_info, parent.clone());
607        self.exprs(else_body.into_iter());
608        // else_branch
609        self.join(if_branch);
610
611        Some(())
612    }
613
614    fn value(&mut self, expr: ast::Expr) -> Option<()> {
615        match self.expr_context {
616            ExprContext::Block => {}
617            ExprContext::BlockExpr => return None,
618            ExprContext::Expr => return None,
619        }
620
621        let ri = self.return_block_info.as_mut()?;
622        if ri.warned {
623            return None;
624        }
625        if matches!(expr, ast::Expr::None(..)) || expr.to_untyped().kind().is_trivia() {
626            return None;
627        }
628
629        if ri.return_value {
630            ri.warned = true;
631            let diag = SourceDiagnostic::warning(
632                expr.span(),
633                eco_format!(
634                    "This {} is implicitly discarded by function return",
635                    expr.to_untyped().kind().name()
636                ),
637            );
638            let diag = match expr {
639                ast::Expr::ShowRule(..) | ast::Expr::SetRule(..) => diag,
640                expr if expr.hash() => diag.with_hint(eco_format!(
641                    "consider ignoring the value explicitly using underscore: `let _ = {}`",
642                    expr.to_untyped().clone().into_text()
643                )),
644                _ => diag,
645            };
646            self.linter.diag.push(diag);
647        } else if ri.return_none && matches!(expr, ast::Expr::ShowRule(..) | ast::Expr::SetRule(..))
648        {
649            ri.warned = true;
650            let diag = SourceDiagnostic::warning(
651                expr.span(),
652                eco_format!(
653                    "This {} is implicitly discarded by function return",
654                    expr.to_untyped().kind().name()
655                ),
656            );
657            self.linter.diag.push(diag);
658        }
659
660        Some(())
661    }
662
663    fn show(&mut self, expr: ast::ShowRule<'_>) -> Option<()> {
664        self.value(ast::Expr::ShowRule(expr));
665        Some(())
666    }
667
668    fn set(&mut self, expr: ast::SetRule<'_>) -> Option<()> {
669        self.value(ast::Expr::SetRule(expr));
670        Some(())
671    }
672
673    fn for_loop(&mut self, expr: ast::ForLoop<'_>) -> Option<()> {
674        self.expr(expr.body())
675    }
676
677    fn while_loop(&mut self, expr: ast::WhileLoop<'_>) -> Option<()> {
678        self.expr(expr.body())
679    }
680}
681
682#[derive(Clone, Default)]
683struct ReturnBlockInfo {
684    return_value: bool,
685    return_none: bool,
686    warned: bool,
687}
688
689impl ReturnBlockInfo {
690    fn merge(self, other: Self) -> Self {
691        Self {
692            return_value: self.return_value && other.return_value,
693            return_none: self.return_none && other.return_none,
694            warned: self.warned && other.warned,
695        }
696    }
697}
698
699trait DataFlowVisitor {
700    fn expr(&mut self, expr: ast::Expr) -> Option<()> {
701        match expr {
702            ast::Expr::Parenthesized(expr) => self.expr(expr.expr()),
703            ast::Expr::CodeBlock(expr) => self.block(expr.body().exprs()),
704            ast::Expr::ContentBlock(expr) => self.block(expr.body().exprs()),
705            ast::Expr::Math(expr) => self.exprs(expr.exprs()),
706
707            ast::Expr::Text(..) => self.value(expr),
708            ast::Expr::Space(..) => self.value(expr),
709            ast::Expr::Linebreak(..) => self.value(expr),
710            ast::Expr::Parbreak(..) => self.value(expr),
711            ast::Expr::Escape(..) => self.value(expr),
712            ast::Expr::Shorthand(..) => self.value(expr),
713            ast::Expr::SmartQuote(..) => self.value(expr),
714            ast::Expr::Raw(..) => self.value(expr),
715            ast::Expr::Link(..) => self.value(expr),
716
717            ast::Expr::Label(..) => self.value(expr),
718            ast::Expr::Ref(..) => self.value(expr),
719            ast::Expr::None(..) => self.value(expr),
720            ast::Expr::Auto(..) => self.value(expr),
721            ast::Expr::Bool(..) => self.value(expr),
722            ast::Expr::Int(..) => self.value(expr),
723            ast::Expr::Float(..) => self.value(expr),
724            ast::Expr::Numeric(..) => self.value(expr),
725            ast::Expr::Str(..) => self.value(expr),
726            ast::Expr::MathText(..) => self.value(expr),
727            ast::Expr::MathShorthand(..) => self.value(expr),
728            ast::Expr::MathAlignPoint(..) => self.value(expr),
729            ast::Expr::MathPrimes(..) => self.value(expr),
730            ast::Expr::MathRoot(..) => self.value(expr),
731
732            ast::Expr::Strong(content) => self.exprs(content.body().exprs()),
733            ast::Expr::Emph(content) => self.exprs(content.body().exprs()),
734            ast::Expr::Heading(content) => self.exprs(content.body().exprs()),
735            ast::Expr::ListItem(content) => self.exprs(content.body().exprs()),
736            ast::Expr::EnumItem(content) => self.exprs(content.body().exprs()),
737            ast::Expr::TermItem(content) => {
738                self.exprs(content.term().exprs().chain(content.description().exprs()))
739            }
740            ast::Expr::MathDelimited(content) => self.exprs(content.body().exprs()),
741            ast::Expr::MathAttach(..) | ast::Expr::MathFrac(..) => self.exprs(expr.exprs()),
742
743            ast::Expr::Ident(expr) => self.ident(expr),
744            ast::Expr::MathIdent(expr) => self.math_ident(expr),
745            ast::Expr::Equation(expr) => self.equation(expr),
746            ast::Expr::Array(expr) => self.array(expr),
747            ast::Expr::Dict(expr) => self.dict(expr),
748            ast::Expr::Unary(expr) => self.unary(expr),
749            ast::Expr::Binary(expr) => self.binary(expr),
750            ast::Expr::FieldAccess(expr) => self.field_access(expr),
751            ast::Expr::FuncCall(expr) => self.func_call(expr),
752            ast::Expr::Closure(expr) => self.closure(expr),
753            ast::Expr::LetBinding(expr) => self.let_binding(expr),
754            ast::Expr::DestructAssignment(expr) => self.destruct_assign(expr),
755            ast::Expr::SetRule(expr) => self.set(expr),
756            ast::Expr::ShowRule(expr) => self.show(expr),
757            ast::Expr::Contextual(expr) => self.contextual(expr),
758            ast::Expr::Conditional(expr) => self.conditional(expr),
759            ast::Expr::WhileLoop(expr) => self.while_loop(expr),
760            ast::Expr::ForLoop(expr) => self.for_loop(expr),
761            ast::Expr::ModuleImport(expr) => self.import(expr),
762            ast::Expr::ModuleInclude(expr) => self.include(expr),
763            ast::Expr::LoopBreak(expr) => self.loop_break(expr),
764            ast::Expr::LoopContinue(expr) => self.loop_continue(expr),
765            ast::Expr::FuncReturn(expr) => self.func_return(expr),
766        }
767    }
768
769    fn exprs<'a>(&mut self, exprs: impl DoubleEndedIterator<Item = ast::Expr<'a>>) -> Option<()> {
770        for expr in exprs {
771            self.expr(expr);
772        }
773        Some(())
774    }
775
776    fn block<'a>(&mut self, exprs: impl DoubleEndedIterator<Item = ast::Expr<'a>>) -> Option<()> {
777        self.exprs(exprs)
778    }
779
780    fn value(&mut self, _expr: ast::Expr) -> Option<()> {
781        Some(())
782    }
783
784    fn ident(&mut self, _expr: ast::Ident<'_>) -> Option<()> {
785        Some(())
786    }
787
788    fn math_ident(&mut self, _expr: ast::MathIdent<'_>) -> Option<()> {
789        Some(())
790    }
791
792    fn import(&mut self, _expr: ast::ModuleImport<'_>) -> Option<()> {
793        Some(())
794    }
795
796    fn include(&mut self, _expr: ast::ModuleInclude<'_>) -> Option<()> {
797        Some(())
798    }
799
800    fn equation(&mut self, expr: ast::Equation<'_>) -> Option<()> {
801        self.exprs(expr.body().exprs())
802    }
803
804    fn array(&mut self, expr: ast::Array<'_>) -> Option<()> {
805        self.exprs(expr.to_untyped().exprs())
806    }
807
808    fn dict(&mut self, expr: ast::Dict<'_>) -> Option<()> {
809        self.exprs(expr.to_untyped().exprs())
810    }
811
812    fn unary(&mut self, expr: ast::Unary<'_>) -> Option<()> {
813        self.expr(expr.expr())
814    }
815
816    fn binary(&mut self, expr: ast::Binary<'_>) -> Option<()> {
817        self.exprs([expr.lhs(), expr.rhs()].into_iter())
818    }
819
820    fn field_access(&mut self, expr: ast::FieldAccess<'_>) -> Option<()> {
821        self.expr(expr.target())
822    }
823
824    fn func_call(&mut self, expr: ast::FuncCall<'_>) -> Option<()> {
825        self.exprs(expr.args().to_untyped().exprs().chain(expr.callee().once()))
826    }
827
828    fn closure(&mut self, expr: ast::Closure<'_>) -> Option<()> {
829        let ident = expr.name().map(ast::Expr::Ident).into_iter();
830        let params = expr.params().to_untyped().exprs();
831        let body = expr.body().once();
832        self.exprs(ident.chain(params).chain(body))
833    }
834
835    fn let_binding(&mut self, expr: ast::LetBinding<'_>) -> Option<()> {
836        self.expr(expr.init()?)
837    }
838
839    fn destruct_assign(&mut self, expr: ast::DestructAssignment<'_>) -> Option<()> {
840        self.expr(expr.value())
841    }
842
843    fn set(&mut self, expr: ast::SetRule<'_>) -> Option<()> {
844        let cond = expr.condition().into_iter();
845        let args = expr.args().to_untyped().exprs();
846        self.exprs(cond.chain(args).chain(expr.target().once()))
847    }
848
849    fn show(&mut self, expr: ast::ShowRule<'_>) -> Option<()> {
850        let selector = expr.selector().into_iter();
851        let transform = expr.transform();
852        self.exprs(selector.chain(transform.once()))
853    }
854
855    fn contextual(&mut self, expr: ast::Contextual<'_>) -> Option<()> {
856        self.expr(expr.body())
857    }
858
859    fn conditional(&mut self, expr: ast::Conditional<'_>) -> Option<()> {
860        let cond = expr.condition().once();
861        let if_body = expr.if_body().once();
862        let else_body = expr.else_body().into_iter();
863        self.exprs(cond.chain(if_body).chain(else_body))
864    }
865
866    fn while_loop(&mut self, expr: ast::WhileLoop<'_>) -> Option<()> {
867        let cond = expr.condition().once();
868        let body = expr.body().once();
869        self.exprs(cond.chain(body))
870    }
871
872    fn for_loop(&mut self, expr: ast::ForLoop<'_>) -> Option<()> {
873        let iterable = expr.iterable().once();
874        let body = expr.body().once();
875        self.exprs(iterable.chain(body))
876    }
877
878    fn loop_break(&mut self, _expr: ast::LoopBreak<'_>) -> Option<()> {
879        Some(())
880    }
881
882    fn loop_continue(&mut self, _expr: ast::LoopContinue<'_>) -> Option<()> {
883        Some(())
884    }
885
886    fn func_return(&mut self, expr: ast::FuncReturn<'_>) -> Option<()> {
887        self.expr(expr.body()?)
888    }
889}
890
891trait ExprsUntyped {
892    fn exprs(&self) -> impl DoubleEndedIterator<Item = ast::Expr<'_>>;
893}
894
895impl ExprsUntyped for ast::Expr<'_> {
896    fn exprs(&self) -> impl DoubleEndedIterator<Item = ast::Expr<'_>> {
897        self.to_untyped().exprs()
898    }
899}
900
901impl ExprsUntyped for SyntaxNode {
902    fn exprs(&self) -> impl DoubleEndedIterator<Item = ast::Expr<'_>> {
903        self.children().filter_map(SyntaxNode::cast)
904    }
905}
906
907trait ExprsOnce<'a> {
908    fn once(self) -> impl DoubleEndedIterator<Item = ast::Expr<'a>>;
909}
910
911impl<'a> ExprsOnce<'a> for ast::Expr<'a> {
912    fn once(self) -> impl DoubleEndedIterator<Item = ast::Expr<'a>> {
913        std::iter::once(self)
914    }
915}
916
917#[derive(Clone)]
918struct LoopInfo {
919    span: Span,
920    has_break: bool,
921    has_continue: bool,
922}
923
924#[derive(Clone)]
925struct FuncInfo {
926    span: Span,
927    is_contextual: bool,
928    has_return: bool,
929    has_return_value: bool,
930    parent_loop: Option<LoopInfo>,
931}
932
933#[derive(Clone, Copy)]
934enum Block<'a> {
935    Code(ast::Code<'a>),
936    Markup(ast::Markup<'a>),
937}
938
939impl<'a> Block<'a> {
940    fn from(expr: ast::Expr<'a>) -> Option<Self> {
941        Some(match expr {
942            ast::Expr::CodeBlock(block) => Block::Code(block.body()),
943            ast::Expr::ContentBlock(block) => Block::Markup(block.body()),
944            _ => return None,
945        })
946    }
947
948    #[inline(always)]
949    fn iter(&self) -> impl Iterator<Item = ast::Expr<'a>> {
950        let (x, y) = match self {
951            Block::Code(block) => (Some(block.exprs()), None),
952            Block::Markup(block) => (None, Some(block.exprs())),
953        };
954
955        x.into_iter().flatten().chain(y.into_iter().flatten())
956    }
957}
958
959#[derive(Debug, Clone)]
960struct TypedExpr<'a> {
961    expr: ast::Expr<'a>,
962    ty: Option<Ty>,
963}
964
965impl TypedExpr<'_> {
966    fn is_str(&self, ctx: &impl TyCtx) -> bool {
967        self.ty
968            .as_ref()
969            .map(|ty| ty.is_str(ctx))
970            .unwrap_or_else(|| matches!(self.expr, ast::Expr::Str(..)))
971    }
972
973    fn is_type(&self, ctx: &impl TyCtx) -> bool {
974        self.ty
975            .as_ref()
976            .map(|ty| ty.is_type(ctx))
977            .unwrap_or_default()
978    }
979}
980
981enum BuggyBlockLoc<'a> {
982    Show(ast::ShowRule<'a>),
983    IfTrue(ast::Conditional<'a>),
984    IfFalse(ast::Conditional<'a>),
985    While(ast::WhileLoop<'a>),
986    For(ast::ForLoop<'a>),
987}
988
989impl BuggyBlockLoc<'_> {
990    fn hint(&self, show_set: ast::Expr<'_>) -> EcoString {
991        match self {
992            BuggyBlockLoc::Show(show_parent) => {
993                if let ast::Expr::ShowRule(show) = show_set {
994                    eco_format!(
995                        "consider changing parent to `show {}: it => {{ {}; it }}`",
996                        match show_parent.selector() {
997                            Some(selector) => selector.to_untyped().clone().into_text(),
998                            None => "".into(),
999                        },
1000                        show.to_untyped().clone().into_text()
1001                    )
1002                } else {
1003                    eco_format!(
1004                        "consider changing parent to `show {}: {}`",
1005                        match show_parent.selector() {
1006                            Some(selector) => selector.to_untyped().clone().into_text(),
1007                            None => "".into(),
1008                        },
1009                        show_set.to_untyped().clone().into_text()
1010                    )
1011                }
1012            }
1013            BuggyBlockLoc::IfTrue(conditional) | BuggyBlockLoc::IfFalse(conditional) => {
1014                let neg = if matches!(self, BuggyBlockLoc::IfTrue(..)) {
1015                    ""
1016                } else {
1017                    "not "
1018                };
1019                if let ast::Expr::ShowRule(show) = show_set {
1020                    eco_format!(
1021                        "consider changing parent to `show {}: if {neg}({}) {{ .. }}`",
1022                        match show.selector() {
1023                            Some(selector) => selector.to_untyped().clone().into_text(),
1024                            None => "".into(),
1025                        },
1026                        conditional.condition().to_untyped().clone().into_text()
1027                    )
1028                } else {
1029                    eco_format!(
1030                        "consider changing parent to `{} if {neg}({})`",
1031                        show_set.to_untyped().clone().into_text(),
1032                        conditional.condition().to_untyped().clone().into_text()
1033                    )
1034                }
1035            }
1036            BuggyBlockLoc::While(w) => {
1037                eco_format!(
1038                    "consider changing parent to `show: it => if {} {{ {}; it }}`",
1039                    w.condition().to_untyped().clone().into_text(),
1040                    show_set.to_untyped().clone().into_text()
1041                )
1042            }
1043            BuggyBlockLoc::For(f) => {
1044                eco_format!(
1045                    "consider changing parent to `show: {}.fold(it => it, (style-it, {}) => it => {{ {}; style-it(it) }})`",
1046                    f.iterable().to_untyped().clone().into_text(),
1047                    f.pattern().to_untyped().clone().into_text(),
1048                    show_set.to_untyped().clone().into_text()
1049                )
1050            }
1051        }
1052    }
1053}
1054
1055#[derive(Clone, Copy, PartialEq, Eq)]
1056enum ExprContext {
1057    BlockExpr,
1058    Block,
1059    Expr,
1060}
1061
1062fn is_show_set(it: ast::Expr) -> bool {
1063    matches!(it, ast::Expr::SetRule(..) | ast::Expr::ShowRule(..))
1064}
1065
1066fn is_compare_op(op: ast::BinOp) -> bool {
1067    use ast::BinOp::*;
1068    matches!(op, Lt | Leq | Gt | Geq | Eq | Neq)
1069}
1070
1071/// The error message when a variable wasn't found it math.
1072#[cold]
1073fn hint_unknown_variable_math(var: &str, in_global: bool, diag: &mut SourceDiagnostic) {
1074    if matches!(var, "none" | "auto" | "false" | "true") {
1075        diag.hint(eco_format!(
1076            "if you meant to use a literal, \
1077             try adding a hash before it: `#{var}`",
1078        ));
1079    } else if in_global {
1080        diag.hint(eco_format!(
1081            "`{var}` is not available directly in math, \
1082             try adding a hash before it: `#{var}`",
1083        ));
1084    } else {
1085        diag.hint(eco_format!(
1086            "if you meant to display multiple letters as is, \
1087             try adding spaces between each letter: `{}`",
1088            var.chars()
1089                .flat_map(|c| [' ', c])
1090                .skip(1)
1091                .collect::<EcoString>()
1092        ));
1093        diag.hint(eco_format!(
1094            "or if you meant to display this as text, \
1095             try placing it in quotes: `\"{var}\"`"
1096        ));
1097    }
1098}