tinymist_lint/
lib.rs

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