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