tinymist_lint/
lib.rs

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