Skip to main content

runmat_static_analysis/lints/
data_api.rs

1use crate::schema::{
2    normalize_literal_string, DatasetSchema, DatasetSchemaProvider, FsDatasetSchemaProvider,
3};
4use runmat_hir::{
5    HirClassMember, HirDiagnostic, HirDiagnosticSeverity, HirExpr, HirExprKind, HirLValue, HirStmt,
6    LoweringResult, VarId,
7};
8use std::collections::{HashMap, HashSet};
9
10#[derive(Clone)]
11struct DatasetBinding {
12    arrays: HashMap<String, usize>,
13}
14
15#[derive(Clone)]
16struct ArrayBinding {
17    array_name: String,
18    rank: Option<usize>,
19}
20
21pub fn lint_data_api(result: &LoweringResult) -> Vec<HirDiagnostic> {
22    let provider = FsDatasetSchemaProvider;
23    lint_data_api_with_provider(result, &provider)
24}
25
26pub fn lint_data_api_with_provider(
27    result: &LoweringResult,
28    provider: &dyn DatasetSchemaProvider,
29) -> Vec<HirDiagnostic> {
30    let mut diags = Vec::new();
31    let mut non_tx_write_count = 0usize;
32    let mut tx_vars = HashSet::<VarId>::new();
33    collect_tx_bindings_from_stmts(&result.hir.body, &mut tx_vars);
34    for stmt in &result.hir.body {
35        walk_stmt_general(stmt, &mut diags, &mut non_tx_write_count, &tx_vars);
36    }
37
38    let mut datasets = HashMap::<VarId, DatasetBinding>::new();
39    let mut arrays = HashMap::<VarId, ArrayBinding>::new();
40    for stmt in &result.hir.body {
41        analyze_stmt_bindings(
42            stmt,
43            result,
44            provider,
45            &mut datasets,
46            &mut arrays,
47            &mut diags,
48        );
49    }
50
51    diags
52}
53
54fn collect_tx_bindings_from_stmts(stmts: &[HirStmt], tx_vars: &mut HashSet<VarId>) {
55    for stmt in stmts {
56        match stmt {
57            HirStmt::Assign(var_id, expr, _, _)
58            | HirStmt::AssignLValue(HirLValue::Var(var_id), expr, _, _) => {
59                if expr_is_begin_call(expr) {
60                    tx_vars.insert(*var_id);
61                }
62            }
63            HirStmt::MultiAssign(var_ids, expr, _, _) => {
64                if expr_is_begin_call(expr) {
65                    for var_id in var_ids.iter().flatten() {
66                        tx_vars.insert(*var_id);
67                    }
68                }
69            }
70            HirStmt::If {
71                then_body,
72                elseif_blocks,
73                else_body,
74                ..
75            } => {
76                collect_tx_bindings_from_stmts(then_body, tx_vars);
77                for (_, body) in elseif_blocks {
78                    collect_tx_bindings_from_stmts(body, tx_vars);
79                }
80                if let Some(body) = else_body {
81                    collect_tx_bindings_from_stmts(body, tx_vars);
82                }
83            }
84            HirStmt::While { body, .. }
85            | HirStmt::For { body, .. }
86            | HirStmt::Function { body, .. } => {
87                collect_tx_bindings_from_stmts(body, tx_vars);
88            }
89            HirStmt::Switch {
90                cases, otherwise, ..
91            } => {
92                for (_, body) in cases {
93                    collect_tx_bindings_from_stmts(body, tx_vars);
94                }
95                if let Some(body) = otherwise {
96                    collect_tx_bindings_from_stmts(body, tx_vars);
97                }
98            }
99            HirStmt::TryCatch {
100                try_body,
101                catch_body,
102                ..
103            } => {
104                collect_tx_bindings_from_stmts(try_body, tx_vars);
105                collect_tx_bindings_from_stmts(catch_body, tx_vars);
106            }
107            HirStmt::ClassDef { members, .. } => {
108                for member in members {
109                    if let HirClassMember::Methods { body, .. } = member {
110                        collect_tx_bindings_from_stmts(body, tx_vars);
111                    }
112                }
113            }
114            HirStmt::ExprStmt(_, _, _)
115            | HirStmt::Break(_)
116            | HirStmt::Continue(_)
117            | HirStmt::Return(_)
118            | HirStmt::Global(_, _)
119            | HirStmt::Persistent(_, _)
120            | HirStmt::Import { .. }
121            | HirStmt::AssignLValue(_, _, _, _) => {}
122        }
123    }
124}
125
126fn expr_is_begin_call(expr: &HirExpr) -> bool {
127    match &expr.kind {
128        HirExprKind::MethodCall(_, method, _) | HirExprKind::DottedInvoke(_, method, _) => {
129            method == "begin"
130        }
131        HirExprKind::FuncCall(name, _) => name == "Dataset.begin",
132        _ => false,
133    }
134}
135
136fn literal_string(expr: &HirExpr) -> Option<String> {
137    match &expr.kind {
138        HirExprKind::String(s) => Some(normalize_literal_string(s)),
139        _ => None,
140    }
141}
142
143fn slice_rank(expr: &HirExpr) -> Option<usize> {
144    match &expr.kind {
145        HirExprKind::Cell(rows) => Some(rows.iter().map(|row| row.len()).sum()),
146        HirExprKind::Colon => Some(1),
147        _ => None,
148    }
149}
150
151fn infer_dataset_binding(
152    provider: &dyn DatasetSchemaProvider,
153    path: &str,
154) -> Option<DatasetBinding> {
155    let schema: DatasetSchema = provider.load_schema(path)?;
156    Some(DatasetBinding {
157        arrays: schema.arrays,
158    })
159}
160
161fn infer_binding_from_expr(
162    expr: &HirExpr,
163    datasets: &HashMap<VarId, DatasetBinding>,
164    provider: &dyn DatasetSchemaProvider,
165) -> (Option<DatasetBinding>, Option<ArrayBinding>) {
166    match &expr.kind {
167        HirExprKind::FuncCall(name, args) if name == "data.open" => {
168            if let Some(path_expr) = args.first() {
169                if let Some(path) = literal_string(path_expr) {
170                    return (infer_dataset_binding(provider, &path), None);
171                }
172            }
173            (None, None)
174        }
175        HirExprKind::MethodCall(base, method, args)
176        | HirExprKind::DottedInvoke(base, method, args)
177            if method == "array" =>
178        {
179            if let HirExprKind::Var(ds_var) = base.kind {
180                if let Some(dataset) = datasets.get(&ds_var) {
181                    if let Some(name_expr) = args.first() {
182                        if let Some(name) = literal_string(name_expr) {
183                            let rank = dataset.arrays.get(&name).copied();
184                            return (
185                                None,
186                                Some(ArrayBinding {
187                                    array_name: name,
188                                    rank,
189                                }),
190                            );
191                        }
192                    }
193                }
194            }
195            (None, None)
196        }
197        HirExprKind::FuncCall(name, args) if name == "Dataset.array" => {
198            if let Some(base) = args.first() {
199                if let HirExprKind::Var(ds_var) = base.kind {
200                    if let Some(dataset) = datasets.get(&ds_var) {
201                        if let Some(name_expr) = args.get(1) {
202                            if let Some(name) = literal_string(name_expr) {
203                                let rank = dataset.arrays.get(&name).copied();
204                                return (
205                                    None,
206                                    Some(ArrayBinding {
207                                        array_name: name,
208                                        rank,
209                                    }),
210                                );
211                            }
212                        }
213                    }
214                }
215            }
216            (None, None)
217        }
218        _ => (None, None),
219    }
220}
221
222fn analyze_data_expr(
223    expr: &HirExpr,
224    result: &LoweringResult,
225    datasets: &HashMap<VarId, DatasetBinding>,
226    arrays: &HashMap<VarId, ArrayBinding>,
227    diags: &mut Vec<HirDiagnostic>,
228) {
229    match &expr.kind {
230        HirExprKind::MethodCall(base, method, args)
231        | HirExprKind::DottedInvoke(base, method, args) => {
232            if method == "array" {
233                if let HirExprKind::Var(ds_var) = base.kind {
234                    if let Some(dataset) = datasets.get(&ds_var) {
235                        if let Some(name_expr) = args.first() {
236                            if let Some(name) = literal_string(name_expr) {
237                                if !dataset.arrays.contains_key(&name) {
238                                    diags.push(HirDiagnostic {
239                                        message: format!(
240                                            "array '{name}' is not present in inferred dataset schema"
241                                        ),
242                                        span: name_expr.span,
243                                        code: "lint.data.unknown_array_name",
244                                        severity: HirDiagnosticSeverity::Warning,
245                                    });
246                                }
247                            }
248                        }
249                    }
250                }
251            }
252
253            if method == "read" || method == "write" {
254                if let HirExprKind::Var(array_var) = base.kind {
255                    if let Some(array_binding) = arrays.get(&array_var) {
256                        if let Some(rank) = array_binding.rank {
257                            let slice_arg = args.first();
258                            if let Some(slice_expr) = slice_arg {
259                                if let Some(actual_slice_rank) = slice_rank(slice_expr) {
260                                    if actual_slice_rank > rank {
261                                        let array_var_name = result
262                                            .var_names
263                                            .get(&array_var)
264                                            .cloned()
265                                            .unwrap_or_else(|| format!("v{}", array_var.0));
266                                        diags.push(HirDiagnostic {
267                                            message: format!(
268                                                "slice rank {actual_slice_rank} exceeds array rank {rank} for '{array_var_name}' ({})",
269                                                array_binding.array_name
270                                            ),
271                                            span: slice_expr.span,
272                                            code: "lint.data.invalid_slice_rank",
273                                            severity: HirDiagnosticSeverity::Warning,
274                                        });
275                                    }
276                                }
277                            }
278                        }
279                    }
280                }
281            }
282
283            analyze_data_expr(base, result, datasets, arrays, diags);
284            for arg in args {
285                analyze_data_expr(arg, result, datasets, arrays, diags);
286            }
287        }
288        HirExprKind::FuncCall(name, args) => {
289            if name == "Dataset.array" {
290                if let Some(base) = args.first() {
291                    if let HirExprKind::Var(ds_var) = base.kind {
292                        if let Some(dataset) = datasets.get(&ds_var) {
293                            if let Some(name_expr) = args.get(1) {
294                                if let Some(array_name) = literal_string(name_expr) {
295                                    if !dataset.arrays.contains_key(&array_name) {
296                                        diags.push(HirDiagnostic {
297                                            message: format!(
298                                                "array '{array_name}' is not present in inferred dataset schema"
299                                            ),
300                                            span: name_expr.span,
301                                            code: "lint.data.unknown_array_name",
302                                            severity: HirDiagnosticSeverity::Warning,
303                                        });
304                                    }
305                                }
306                            }
307                        }
308                    }
309                }
310            }
311
312            if name == "DataArray.read" || name == "DataArray.write" {
313                if let Some(base) = args.first() {
314                    if let HirExprKind::Var(array_var) = base.kind {
315                        if let Some(array_binding) = arrays.get(&array_var) {
316                            if let Some(rank) = array_binding.rank {
317                                let slice_arg = args.get(1);
318                                if let Some(slice_expr) = slice_arg {
319                                    if let Some(actual_slice_rank) = slice_rank(slice_expr) {
320                                        if actual_slice_rank > rank {
321                                            let array_var_name = result
322                                                .var_names
323                                                .get(&array_var)
324                                                .cloned()
325                                                .unwrap_or_else(|| format!("v{}", array_var.0));
326                                            diags.push(HirDiagnostic {
327                                                message: format!(
328                                                    "slice rank {actual_slice_rank} exceeds array rank {rank} for '{array_var_name}' ({})",
329                                                    array_binding.array_name
330                                                ),
331                                                span: slice_expr.span,
332                                                code: "lint.data.invalid_slice_rank",
333                                                severity: HirDiagnosticSeverity::Warning,
334                                            });
335                                        }
336                                    }
337                                }
338                            }
339                        }
340                    }
341                }
342            }
343
344            for arg in args {
345                analyze_data_expr(arg, result, datasets, arrays, diags);
346            }
347        }
348        HirExprKind::Unary(_, inner) => analyze_data_expr(inner, result, datasets, arrays, diags),
349        HirExprKind::Binary(lhs, _, rhs) => {
350            analyze_data_expr(lhs, result, datasets, arrays, diags);
351            analyze_data_expr(rhs, result, datasets, arrays, diags);
352        }
353        HirExprKind::Tensor(rows) | HirExprKind::Cell(rows) => {
354            for row in rows {
355                for value in row {
356                    analyze_data_expr(value, result, datasets, arrays, diags);
357                }
358            }
359        }
360        HirExprKind::Index(base, args) | HirExprKind::IndexCell(base, args) => {
361            analyze_data_expr(base, result, datasets, arrays, diags);
362            for arg in args {
363                analyze_data_expr(arg, result, datasets, arrays, diags);
364            }
365        }
366        HirExprKind::Range(start, step, end) => {
367            analyze_data_expr(start, result, datasets, arrays, diags);
368            if let Some(step) = step {
369                analyze_data_expr(step, result, datasets, arrays, diags);
370            }
371            analyze_data_expr(end, result, datasets, arrays, diags);
372        }
373        HirExprKind::Member(base, _) | HirExprKind::AnonFunc { body: base, .. } => {
374            analyze_data_expr(base, result, datasets, arrays, diags)
375        }
376        HirExprKind::MemberDynamic(base, field) => {
377            analyze_data_expr(base, result, datasets, arrays, diags);
378            analyze_data_expr(field, result, datasets, arrays, diags);
379        }
380        HirExprKind::FuncHandle(_)
381        | HirExprKind::MetaClass(_)
382        | HirExprKind::Var(_)
383        | HirExprKind::String(_)
384        | HirExprKind::Number(_)
385        | HirExprKind::Constant(_)
386        | HirExprKind::Colon
387        | HirExprKind::End => {}
388    }
389}
390
391fn analyze_stmt_bindings(
392    stmt: &HirStmt,
393    result: &LoweringResult,
394    provider: &dyn DatasetSchemaProvider,
395    datasets: &mut HashMap<VarId, DatasetBinding>,
396    arrays: &mut HashMap<VarId, ArrayBinding>,
397    diags: &mut Vec<HirDiagnostic>,
398) {
399    match stmt {
400        HirStmt::ExprStmt(expr, _, _) => analyze_data_expr(expr, result, datasets, arrays, diags),
401        HirStmt::Assign(var, expr, _, _) => {
402            analyze_data_expr(expr, result, datasets, arrays, diags);
403            let (dataset_binding, array_binding) =
404                infer_binding_from_expr(expr, datasets, provider);
405            if let Some(binding) = dataset_binding {
406                datasets.insert(*var, binding);
407                arrays.remove(var);
408                return;
409            }
410            if let Some(binding) = array_binding {
411                arrays.insert(*var, binding);
412                datasets.remove(var);
413                return;
414            }
415            datasets.remove(var);
416            arrays.remove(var);
417        }
418        HirStmt::MultiAssign(_, expr, _, _) => {
419            analyze_data_expr(expr, result, datasets, arrays, diags)
420        }
421        HirStmt::AssignLValue(lv, expr, _, _) => {
422            analyze_data_expr(expr, result, datasets, arrays, diags);
423            match lv {
424                HirLValue::Var(var) => {
425                    datasets.remove(var);
426                    arrays.remove(var);
427                }
428                HirLValue::Index(base, args) | HirLValue::IndexCell(base, args) => {
429                    analyze_data_expr(base, result, datasets, arrays, diags);
430                    for arg in args {
431                        analyze_data_expr(arg, result, datasets, arrays, diags);
432                    }
433                }
434                HirLValue::Member(base, _) => {
435                    analyze_data_expr(base, result, datasets, arrays, diags)
436                }
437                HirLValue::MemberDynamic(base, field) => {
438                    analyze_data_expr(base, result, datasets, arrays, diags);
439                    analyze_data_expr(field, result, datasets, arrays, diags);
440                }
441            }
442        }
443        HirStmt::If {
444            cond,
445            then_body,
446            elseif_blocks,
447            else_body,
448            ..
449        } => {
450            analyze_data_expr(cond, result, datasets, arrays, diags);
451            for nested in then_body {
452                analyze_stmt_bindings(nested, result, provider, datasets, arrays, diags);
453            }
454            for (cond, body) in elseif_blocks {
455                analyze_data_expr(cond, result, datasets, arrays, diags);
456                for nested in body {
457                    analyze_stmt_bindings(nested, result, provider, datasets, arrays, diags);
458                }
459            }
460            if let Some(body) = else_body {
461                for nested in body {
462                    analyze_stmt_bindings(nested, result, provider, datasets, arrays, diags);
463                }
464            }
465        }
466        HirStmt::While { cond, body, .. } => {
467            analyze_data_expr(cond, result, datasets, arrays, diags);
468            for nested in body {
469                analyze_stmt_bindings(nested, result, provider, datasets, arrays, diags);
470            }
471        }
472        HirStmt::For { expr, body, .. } => {
473            analyze_data_expr(expr, result, datasets, arrays, diags);
474            for nested in body {
475                analyze_stmt_bindings(nested, result, provider, datasets, arrays, diags);
476            }
477        }
478        HirStmt::Switch {
479            expr,
480            cases,
481            otherwise,
482            ..
483        } => {
484            analyze_data_expr(expr, result, datasets, arrays, diags);
485            for (case_expr, body) in cases {
486                analyze_data_expr(case_expr, result, datasets, arrays, diags);
487                for nested in body {
488                    analyze_stmt_bindings(nested, result, provider, datasets, arrays, diags);
489                }
490            }
491            if let Some(body) = otherwise {
492                for nested in body {
493                    analyze_stmt_bindings(nested, result, provider, datasets, arrays, diags);
494                }
495            }
496        }
497        HirStmt::TryCatch {
498            try_body,
499            catch_body,
500            ..
501        } => {
502            for nested in try_body {
503                analyze_stmt_bindings(nested, result, provider, datasets, arrays, diags);
504            }
505            for nested in catch_body {
506                analyze_stmt_bindings(nested, result, provider, datasets, arrays, diags);
507            }
508        }
509        HirStmt::Function { body, .. } => {
510            for nested in body {
511                analyze_stmt_bindings(nested, result, provider, datasets, arrays, diags);
512            }
513        }
514        HirStmt::ClassDef { members, .. } => {
515            for member in members {
516                if let HirClassMember::Methods { body, .. } = member {
517                    for nested in body {
518                        analyze_stmt_bindings(nested, result, provider, datasets, arrays, diags);
519                    }
520                }
521            }
522        }
523        HirStmt::Break(_)
524        | HirStmt::Continue(_)
525        | HirStmt::Return(_)
526        | HirStmt::Global(_, _)
527        | HirStmt::Persistent(_, _)
528        | HirStmt::Import { .. } => {}
529    }
530}
531
532fn walk_expr_general(
533    expr: &HirExpr,
534    diags: &mut Vec<HirDiagnostic>,
535    non_tx_write_count: &mut usize,
536    tx_vars: &HashSet<VarId>,
537) {
538    match &expr.kind {
539        HirExprKind::FuncCall(name, args) => {
540            if name == "data.open" {
541                let first = args.first();
542                let is_typed_open = args.len() > 1;
543                let first_is_literal =
544                    matches!(first.map(|a| &a.kind), Some(HirExprKind::String(_)));
545                if !first_is_literal && !is_typed_open {
546                    diags.push(HirDiagnostic {
547                        message: "data.open with dynamic path should include explicit schema for type safety"
548                            .to_string(),
549                        span: expr.span,
550                        code: "lint.data.no_untyped_open",
551                        severity: HirDiagnosticSeverity::Warning,
552                    });
553                }
554            }
555            for arg in args {
556                walk_expr_general(arg, diags, non_tx_write_count, tx_vars);
557            }
558        }
559        HirExprKind::MethodCall(base, method, args)
560        | HirExprKind::DottedInvoke(base, method, args) => {
561            if method == "write" {
562                let in_tx =
563                    matches!(base.kind, HirExprKind::Var(var_id) if tx_vars.contains(&var_id));
564                if !in_tx {
565                    *non_tx_write_count += 1;
566                    if *non_tx_write_count > 1 {
567                        diags.push(HirDiagnostic {
568                            message:
569                                "multiple data writes detected outside explicit transaction; consider ds.begin() + tx.commit()"
570                                    .to_string(),
571                            span: expr.span,
572                            code: "lint.data.no_multiwrite_outside_tx",
573                            severity: HirDiagnosticSeverity::Warning,
574                        });
575                    }
576                }
577            }
578            walk_expr_general(base, diags, non_tx_write_count, tx_vars);
579            for arg in args {
580                walk_expr_general(arg, diags, non_tx_write_count, tx_vars);
581            }
582        }
583        HirExprKind::Unary(_, inner) => {
584            walk_expr_general(inner, diags, non_tx_write_count, tx_vars)
585        }
586        HirExprKind::Binary(lhs, _, rhs) => {
587            walk_expr_general(lhs, diags, non_tx_write_count, tx_vars);
588            walk_expr_general(rhs, diags, non_tx_write_count, tx_vars);
589        }
590        HirExprKind::Tensor(rows) | HirExprKind::Cell(rows) => {
591            for row in rows {
592                for value in row {
593                    walk_expr_general(value, diags, non_tx_write_count, tx_vars);
594                }
595            }
596        }
597        HirExprKind::Index(base, args) | HirExprKind::IndexCell(base, args) => {
598            walk_expr_general(base, diags, non_tx_write_count, tx_vars);
599            for arg in args {
600                walk_expr_general(arg, diags, non_tx_write_count, tx_vars);
601            }
602        }
603        HirExprKind::Range(start, step, end) => {
604            walk_expr_general(start, diags, non_tx_write_count, tx_vars);
605            if let Some(step) = step {
606                walk_expr_general(step, diags, non_tx_write_count, tx_vars);
607            }
608            walk_expr_general(end, diags, non_tx_write_count, tx_vars);
609        }
610        HirExprKind::Member(base, _) | HirExprKind::AnonFunc { body: base, .. } => {
611            walk_expr_general(base, diags, non_tx_write_count, tx_vars)
612        }
613        HirExprKind::MemberDynamic(base, field) => {
614            walk_expr_general(base, diags, non_tx_write_count, tx_vars);
615            walk_expr_general(field, diags, non_tx_write_count, tx_vars);
616        }
617        HirExprKind::FuncHandle(_)
618        | HirExprKind::MetaClass(_)
619        | HirExprKind::Var(_)
620        | HirExprKind::String(_)
621        | HirExprKind::Number(_)
622        | HirExprKind::Constant(_)
623        | HirExprKind::Colon
624        | HirExprKind::End => {}
625    }
626}
627
628fn walk_stmt_general(
629    stmt: &HirStmt,
630    diags: &mut Vec<HirDiagnostic>,
631    non_tx_write_count: &mut usize,
632    tx_vars: &HashSet<VarId>,
633) {
634    match stmt {
635        HirStmt::ExprStmt(expr, _, _) => {
636            if matches!(
637                &expr.kind,
638                HirExprKind::MethodCall(_, method, _) | HirExprKind::DottedInvoke(_, method, _)
639                    if method == "commit"
640            ) {
641                diags.push(HirDiagnostic {
642                    message: "consider checking transaction commit outcomes in shared workflows"
643                        .to_string(),
644                    span: expr.span,
645                    code: "lint.data.ignore_commit_result",
646                    severity: HirDiagnosticSeverity::Information,
647                });
648            }
649            walk_expr_general(expr, diags, non_tx_write_count, tx_vars)
650        }
651        HirStmt::Assign(_, expr, _, _) => {
652            walk_expr_general(expr, diags, non_tx_write_count, tx_vars)
653        }
654        HirStmt::MultiAssign(_, expr, _, _) => {
655            walk_expr_general(expr, diags, non_tx_write_count, tx_vars)
656        }
657        HirStmt::AssignLValue(lv, expr, _, _) => {
658            match lv {
659                HirLValue::Var(_) => {}
660                HirLValue::Index(base, args) | HirLValue::IndexCell(base, args) => {
661                    walk_expr_general(base, diags, non_tx_write_count, tx_vars);
662                    for arg in args {
663                        walk_expr_general(arg, diags, non_tx_write_count, tx_vars);
664                    }
665                }
666                HirLValue::Member(base, _) => {
667                    walk_expr_general(base, diags, non_tx_write_count, tx_vars)
668                }
669                HirLValue::MemberDynamic(base, field) => {
670                    walk_expr_general(base, diags, non_tx_write_count, tx_vars);
671                    walk_expr_general(field, diags, non_tx_write_count, tx_vars);
672                }
673            }
674            walk_expr_general(expr, diags, non_tx_write_count, tx_vars);
675        }
676        HirStmt::If {
677            cond,
678            then_body,
679            elseif_blocks,
680            else_body,
681            ..
682        } => {
683            walk_expr_general(cond, diags, non_tx_write_count, tx_vars);
684            for stmt in then_body {
685                walk_stmt_general(stmt, diags, non_tx_write_count, tx_vars);
686            }
687            for (cond, body) in elseif_blocks {
688                walk_expr_general(cond, diags, non_tx_write_count, tx_vars);
689                for stmt in body {
690                    walk_stmt_general(stmt, diags, non_tx_write_count, tx_vars);
691                }
692            }
693            if let Some(body) = else_body {
694                for stmt in body {
695                    walk_stmt_general(stmt, diags, non_tx_write_count, tx_vars);
696                }
697            }
698        }
699        HirStmt::While { cond, body, .. } => {
700            walk_expr_general(cond, diags, non_tx_write_count, tx_vars);
701            for stmt in body {
702                walk_stmt_general(stmt, diags, non_tx_write_count, tx_vars);
703            }
704        }
705        HirStmt::For { expr, body, .. } => {
706            walk_expr_general(expr, diags, non_tx_write_count, tx_vars);
707            for stmt in body {
708                walk_stmt_general(stmt, diags, non_tx_write_count, tx_vars);
709            }
710        }
711        HirStmt::Switch {
712            expr,
713            cases,
714            otherwise,
715            ..
716        } => {
717            walk_expr_general(expr, diags, non_tx_write_count, tx_vars);
718            for (case_expr, body) in cases {
719                walk_expr_general(case_expr, diags, non_tx_write_count, tx_vars);
720                for stmt in body {
721                    walk_stmt_general(stmt, diags, non_tx_write_count, tx_vars);
722                }
723            }
724            if let Some(body) = otherwise {
725                for stmt in body {
726                    walk_stmt_general(stmt, diags, non_tx_write_count, tx_vars);
727                }
728            }
729        }
730        HirStmt::TryCatch {
731            try_body,
732            catch_body,
733            ..
734        } => {
735            for stmt in try_body {
736                walk_stmt_general(stmt, diags, non_tx_write_count, tx_vars);
737            }
738            for stmt in catch_body {
739                walk_stmt_general(stmt, diags, non_tx_write_count, tx_vars);
740            }
741        }
742        HirStmt::Function { body, .. } => {
743            for stmt in body {
744                walk_stmt_general(stmt, diags, non_tx_write_count, tx_vars);
745            }
746        }
747        HirStmt::ClassDef { members, .. } => {
748            for member in members {
749                if let HirClassMember::Methods { body, .. } = member {
750                    for stmt in body {
751                        walk_stmt_general(stmt, diags, non_tx_write_count, tx_vars);
752                    }
753                }
754            }
755        }
756        HirStmt::Break(_)
757        | HirStmt::Continue(_)
758        | HirStmt::Return(_)
759        | HirStmt::Global(_, _)
760        | HirStmt::Persistent(_, _)
761        | HirStmt::Import { .. } => {}
762    }
763}