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