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