loctree 0.8.16

Structural code intelligence for AI agents. Scan once, query everything.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
# React Lint: Race Condition Detection

> Static analysis for React race conditions using OXC AST parser.
> Zero runtime overhead. Zero dependencies. Pure Rust.

---

## Overview

React's `useEffect` hook is deceptively simple but notoriously easy to misuse. Async operations without proper cleanup cause:

- **Memory leaks** - callbacks firing after unmount
- **Race conditions** - stale data overwriting fresh data
- **Zombie state updates** - `setState` on unmounted components
- **Unpredictable UI** - flickering, wrong data, crashes

**React Lint** statically detects these patterns at build time, before they reach production.

---

## Quick Start

```bash
# From your React project directory
loct findings

# Output includes react_lint section:
# {
#   "react_lint": [
#     {
#       "file": "src/hooks/useData.ts",
#       "line": 42,
#       "rule": "react/async-effect-no-cleanup",
#       "severity": "high",
#       "message": "Async useEffect without cleanup...",
#       "suggestion": "Add a cleanup function with cancelled flag..."
#     }
#   ]
# }
```

---

## Detection Rules

### `react/async-effect-no-cleanup` (HIGH)

Detects async operations in useEffect without cleanup mechanism.

```tsx
// BAD - will flag
useEffect(() => {
  const fetchData = async () => {
    const data = await api.getData();
    setData(data);  // May fire after unmount!
  };
  fetchData();
}, []);

// GOOD - won't flag
useEffect(() => {
  let cancelled = false;

  const fetchData = async () => {
    const data = await api.getData();
    if (!cancelled) setData(data);
  };
  fetchData();

  return () => { cancelled = true; };
}, []);
```

**Why it matters:** If component unmounts while `fetchData` is in-flight, `setData` fires on an unmounted component. React warns about this, and it can cause memory leaks.

---

### `react/settimeout-no-cleanup` (MEDIUM)

Detects `setTimeout` calls without corresponding `clearTimeout`.

```tsx
// BAD - will flag
useEffect(() => {
  setTimeout(() => {
    setVisible(true);
  }, 1000);
}, []);

// GOOD - won't flag
useEffect(() => {
  const timer = setTimeout(() => {
    setVisible(true);
  }, 1000);

  return () => clearTimeout(timer);
}, []);
```

---

### `react/setinterval-no-cleanup` (HIGH)

Detects `setInterval` calls without corresponding `clearInterval`.

```tsx
// BAD - will flag (and it's really bad)
useEffect(() => {
  setInterval(() => {
    tick();
  }, 100);
}, []);

// GOOD - won't flag
useEffect(() => {
  const interval = setInterval(() => tick(), 100);
  return () => clearInterval(interval);
}, []);
```

**Why HIGH severity:** Unlike setTimeout, intervals keep firing forever. Missing cleanup = guaranteed memory leak.

---

### `react/event-listener-no-cleanup` (MEDIUM)

Detects `addEventListener` without corresponding `removeEventListener`.

```tsx
// BAD - will flag
useEffect(() => {
  window.addEventListener('resize', handleResize);
}, []);

// GOOD - won't flag
useEffect(() => {
  window.addEventListener('resize', handleResize);
  return () => window.removeEventListener('resize', handleResize);
}, []);
```

---

## Recognized Cleanup Patterns

The analyzer understands these cleanup idioms and **won't flag** code that uses them:

### Cancelled Flag Pattern
```tsx
useEffect(() => {
  let cancelled = false;
  // ...async work...
  return () => { cancelled = true; };
}, []);
```

### Mounted Flag Pattern
```tsx
useEffect(() => {
  let mounted = true;
  // ...
  return () => { mounted = false; };
}, []);
```

### Ref-based Pattern
```tsx
const isMountedRef = useRef(true);
useEffect(() => {
  // ...checks isMountedRef.current...
  return () => { isMountedRef.current = false; };
}, []);
```

### AbortController Pattern
```tsx
useEffect(() => {
  const controller = new AbortController();
  fetch(url, { signal: controller.signal });
  return () => controller.abort();
}, []);
```

### Timer Cleanup
```tsx
useEffect(() => {
  const timer = setTimeout(...);
  return () => clearTimeout(timer);
}, []);
```

### Event Listener Cleanup
```tsx
useEffect(() => {
  window.addEventListener('event', handler);
  return () => window.removeEventListener('event', handler);
}, []);
```

---

## Architecture

```
┌─────────────────────────────────────────────────────────┐
│                    react_lint.rs                        │
├─────────────────────────────────────────────────────────┤
│                                                         │
│  ┌─────────────┐    ┌──────────────┐    ┌───────────┐  │
│  │  OXC Parse  │───▶│  AST Visit   │───▶│  Analyze  │  │
│  │  (fast!)    │    │  useEffect   │    │  cleanup  │  │
│  └─────────────┘    └──────────────┘    └───────────┘  │
│         │                  │                  │         │
│         ▼                  ▼                  ▼         │
│  ┌─────────────────────────────────────────────────┐   │
│  │              ReactLintIssue[]                   │   │
│  │  • file, line, column                           │   │
│  │  • rule (enum)                                  │   │
│  │  • severity (high/medium)                       │   │
│  │  • message + suggestion                         │   │
│  └─────────────────────────────────────────────────┘   │
│                                                         │
└─────────────────────────────────────────────────────────┘
              ┌─────────────────────────┐
              │     findings.json       │
              │  (consolidated output)  │
              └─────────────────────────┘
```

### Why OXC?

- **Speed**: 3-5x faster than tree-sitter for JS/TS
- **Accuracy**: Full TypeScript support, JSX-aware
- **Rust-native**: No FFI overhead, compiles with loctree
- **Battle-tested**: Used by Biome, oxlint, and other production tools

---

## Integration with Findings

React lint results are included in the standard `findings.json` output:

```json
{
  "summary": {
    "react_lint": {
      "total_issues": 13,
      "by_severity": {
        "high": 10,
        "medium": 3
      },
      "by_rule": {
        "react/async-effect-no-cleanup": 10,
        "react/settimeout-no-cleanup": 2,
        "react/setinterval-no-cleanup": 1
      },
      "affected_files": 8
    }
  },
  "react_lint": [
    {
      "file": "src/contexts/AuthContext.tsx",
      "line": 157,
      "column": 3,
      "rule": "react/async-effect-no-cleanup",
      "severity": "high",
      "message": "Async useEffect without cleanup. May cause setState on unmounted component.",
      "suggestion": "Add a cleanup function with a cancelled flag or AbortController."
    }
  ]
}
```

---

## Real-World Validation

Tested across multiple production codebases:

| Project | Type | Files Scanned | Issues Found | False Positives |
|---------|------|---------------|--------------|-----------------|
| Vista (Tauri + React) | Large app | 746 | 9-13* | 0 |
| TheBeingsSpace (Vite) | Medium app | ~200 | 1 | 0 |
| Nextra docs (Next.js) | Docs site | ~50 | 1 | 0 |
| Fresh CNA project | Starter | ~20 | 0 | 0 |

*Varies by branch - older code has more issues*

Every finding was manually verified. **Zero false positives** in testing.

---

## Known Limitations

### Won't Detect

1. **Custom hooks wrapping useEffect**
   ```tsx
   // Won't flag - can't see inside useAsyncEffect
   useAsyncEffect(async () => {
     await fetchData();
   }, []);
   ```

2. **External library patterns**
   - react-query, SWR, Apollo - have built-in cleanup
   - Analyzer doesn't know they're safe

3. **Cleanup in imported functions**
   ```tsx
   // Won't flag - cleanup logic is elsewhere
   useEffect(() => {
     setupSubscription();  // cleanup inside this function
   }, []);
   ```

4. **Non-React frameworks**
   - Vue, Svelte, Angular use different patterns
   - This tool is React-specific

### May Flag (but safe)

1. **Fire-and-forget effects** that intentionally don't cleanup
   ```tsx
   // May flag, but sometimes intentional
   useEffect(() => {
     analytics.track('page_view');  // Don't care if unmounted
   }, []);
   ```

---

## Performance

| Metric | Value |
|--------|-------|
| Parse + analyze 1 file | ~1-2ms |
| Full Vista scan (746 files) | ~2-3s |
| Memory overhead | Negligible |
| Binary size impact | +~50KB |

React lint runs as part of the standard `loct findings` scan. No additional passes required.

---

## Future Roadmap

- [ ] **`react/exhaustive-deps`** - missing dependencies in useEffect
- [ ] **`react/stale-closure`** - closures capturing stale values
- [ ] **Custom hook awareness** - recognize cleanup in common libraries
- [ ] **Auto-fix suggestions** - generate cleanup code
- [ ] **SARIF output** - for IDE/CI integration

---

## API Reference

### `analyze_react_file`

```rust
pub fn analyze_react_file(
    content: &str,
    path: &Path,
    relative_path: String,
) -> Vec<ReactLintIssue>
```

Analyzes a single file for React race conditions.

### `ReactLintIssue`

```rust
pub struct ReactLintIssue {
    pub file: String,
    pub line: usize,
    pub column: usize,
    pub rule: String,        // e.g., "react/async-effect-no-cleanup"
    pub severity: String,    // "high" | "medium"
    pub message: String,
    pub suggestion: Option<String>,
}
```

### `ReactLintRule`

```rust
pub enum ReactLintRule {
    AsyncEffectNoCleanup,    // HIGH - async without cleanup
    SetTimeoutNoCleanup,     // MEDIUM - setTimeout without clearTimeout
    SetIntervalNoCleanup,    // HIGH - setInterval without clearInterval
    EventListenerNoCleanup,  // MEDIUM - addEventListener without remove
}
```

---

## Contributing

React lint lives in `src/analyzer/react_lint.rs`. Key areas for contribution:

1. **New rules** - add to `ReactLintRule` enum and visitor logic
2. **Cleanup patterns** - expand pattern recognition in `check_cleanup_patterns`
3. **Tests** - unit tests at bottom of `react_lint.rs` (11 existing)

Run tests:
```bash
cargo test react_lint
```

---

## Credits

Built with:
- [OXC]https://github.com/oxc-project/oxc - The JavaScript Oxidation Compiler
- Pattern research from React docs, Dan Abramov's blog, and real-world bug hunting