tinymist_lint/
lib.rs

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