1use 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
20type DiagnosticVec = EcoVec<SourceDiagnostic>;
22
23#[derive(Debug, Clone)]
25pub struct LintInfo {
26 pub revision: usize,
28 pub fid: FileId,
30 pub diagnostics: DiagnosticVec,
32}
33
34pub 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#[derive(Default, Clone, Hash)]
53pub struct KnownIssues {
54 unknown_vars: EcoVec<Span>,
55}
56
57impl KnownIssues {
58 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 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 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 *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 let _body = expr.body().once();
565 self.exprs(ident.chain(params))
566 }
567
568 fn contextual(&mut self, expr: ast::Contextual<'_>) -> Option<()> {
569 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 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#[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}