1use 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
19type DiagnosticVec = EcoVec<SourceDiagnostic>;
21
22#[derive(Debug, Clone)]
24pub struct LintInfo {
25 pub revision: usize,
27 pub fid: FileId,
29 pub diagnostics: DiagnosticVec,
31}
32
33pub 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 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 *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 let _body = expr.body().once();
500 self.exprs(ident.chain(params))
501 }
502
503 fn contextual(&mut self, expr: ast::Contextual<'_>) -> Option<()> {
504 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 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}