vize_croquis 0.76.0

Croquis - Semantic analysis layer for Vize. Quick sketches of meaning from Vue templates.
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
//! Rich formatting for cross-file diagnostics.
//!
//! Provides the [`to_markdown`](super::CrossFileDiagnostic::to_markdown) method
//! that renders a human-readable Markdown representation of each diagnostic.

use super::{CrossFileDiagnostic, CrossFileDiagnosticKind, DiagnosticSeverity};
use vize_carton::append;
use vize_carton::String;

impl CrossFileDiagnostic {
    /// Generate rich markdown diagnostic message.
    pub fn to_markdown(&self) -> String {
        let mut out = String::with_capacity(512);

        // Severity badge
        let severity_badge = match self.severity {
            DiagnosticSeverity::Error => "πŸ”΄ **ERROR**",
            DiagnosticSeverity::Warning => "🟑 **WARNING**",
            DiagnosticSeverity::Info => "πŸ”΅ **INFO**",
            DiagnosticSeverity::Hint => "πŸ’‘ **HINT**",
        };

        append!(out, "{severity_badge} `{}`\n\n", self.code());
        append!(out, "### {}\n\n", self.message);

        // Detailed explanation based on kind
        self.format_kind_details(&mut out);

        // Suggestion
        if let Some(suggestion) = &self.suggestion {
            append!(out, "\n**πŸ’‘ Suggestion**: {suggestion}\n");
        }

        out
    }

    /// Write kind-specific detailed explanation into the output buffer.
    fn format_kind_details(&self, out: &mut String) {
        match &self.kind {
            CrossFileDiagnosticKind::ReactivityOutsideSetup {
                api_name,
                context_description,
            } => {
                append!(
                    *out,
                    "**Problem**: `{api_name}()` is called outside the setup context ({context_description}).\n\n",
                );
                out.push_str("**Why this is dangerous**:\n\n");
                out.push_str("- πŸ”„ **State Pollution (CSRP)**: In SSR, module-level state is shared across requests, causing data leaks between users.\n");
                out.push_str("- πŸ’Ύ **Memory Leak**: Reactive state created outside setup won't be cleaned up when the component unmounts.\n");
                out.push_str("- πŸ› **Unpredictable Behavior**: The reactivity system expects to track dependencies within component context.\n\n");
                out.push_str("**Correct usage**:\n\n");
                out.push_str("```vue\n");
                out.push_str("<script setup>\n");
                append!(
                    *out,
                    "const state = {api_name}(...) // βœ… Called in setup\n",
                );
                out.push_str("</script>\n");
                out.push_str("```\n");
            }
            CrossFileDiagnosticKind::LifecycleOutsideSetup {
                hook_name,
                context_description,
            } => {
                append!(
                    *out,
                    "**Problem**: `{hook_name}` is called outside the setup context ({context_description}).\n\n",
                );
                out.push_str("**Why this fails**:\n\n");
                out.push_str(
                    "- Lifecycle hooks must be called **synchronously** during `setup()`.\n",
                );
                out.push_str("- They rely on the current component instance being set.\n");
                out.push_str("- Calling them elsewhere will throw an error or have no effect.\n\n");
            }
            CrossFileDiagnosticKind::WatcherOutsideSetup {
                api_name,
                context_description,
            } => {
                append!(
                    *out,
                    "**Problem**: `{}()` is called outside the setup context ({}).\n\n",
                    api_name,
                    context_description
                );
                out.push_str("**Why this causes memory leaks**:\n\n");
                out.push_str("- Watchers created in setup are **automatically stopped** when the component unmounts.\n");
                out.push_str(
                    "- Watchers created outside setup **run forever** until manually stopped.\n",
                );
                out.push_str("- Each component mount creates new watchers without cleanup β†’ memory leak.\n\n");
                out.push_str("**If you need a global watcher**, store the stop handle:\n\n");
                out.push_str("```ts\n");
                append!(*out, "const stop = {api_name}(...)\n");
                out.push_str("// Later: stop()\n");
                out.push_str("```\n");
            }
            CrossFileDiagnosticKind::SpreadBreaksReactivity {
                source_name,
                source_type,
            } => {
                append!(
                    *out,
                    "**Problem**: Spreading `{source_name}` (a `{source_type}`) creates a **non-reactive shallow copy**.\n\n",
                );
                out.push_str("**What happens**:\n\n");
                out.push_str("```ts\n");
                append!(
                    *out,
                    "const copy = {{ ...{source_name} }} // ❌ copy is NOT reactive\n",
                );
                append!(
                    *out,
                    "{source_name}.foo = 'bar' // copy.foo is still the old value\n",
                );
                out.push_str("```\n\n");
                out.push_str("**Fix**: Keep the reference, or use `toRefs()`:\n\n");
                out.push_str("```ts\n");
                append!(
                    *out,
                    "const {{ foo, bar }} = toRefs({source_name}) // βœ… foo, bar are refs\n",
                );
                out.push_str("```\n");
            }
            CrossFileDiagnosticKind::ReassignmentBreaksReactivity {
                variable_name,
                original_type,
            } => {
                append!(
                    *out,
                    "**Problem**: Reassigning `{variable_name}` loses the original `{original_type}` reference.\n\n",
                );
                out.push_str("**What happens**:\n\n");
                out.push_str("```ts\n");
                append!(*out, "let {variable_name} = ref(0)\n");
                append!(
                    *out,
                    "{variable_name} = ref(1) // ❌ Template still watches the OLD ref\n",
                );
                out.push_str("```\n\n");
                out.push_str("**Fix**: Mutate the `.value` instead:\n\n");
                out.push_str("```ts\n");
                append!(*out, "const {variable_name} = ref(0)\n");
                append!(
                    *out,
                    "{variable_name}.value = 1 // βœ… Same ref, new value\n",
                );
                out.push_str("```\n");
            }
            CrossFileDiagnosticKind::DestructuringBreaksReactivity {
                source_name,
                destructured_keys,
                suggestion,
            } => {
                append!(
                    *out,
                    "**Problem**: Destructuring `{source_name}` extracts plain values, losing reactivity.\n\n",
                );
                out.push_str("**What happens**:\n\n");
                out.push_str("```ts\n");
                let keys = destructured_keys
                    .iter()
                    .map(|k| k.as_str())
                    .collect::<Vec<_>>()
                    .join(", ");
                append!(
                    *out,
                    "const {{ {keys} }} = {source_name} // ❌ {keys} are plain values\n",
                );
                out.push_str("```\n\n");
                append!(*out, "**Fix**: Use `{suggestion}()`:\n\n");
                out.push_str("```ts\n");
                append!(
                    *out,
                    "const {{ {keys} }} = {suggestion}({source_name}) // βœ… {keys} are refs\n",
                );
                out.push_str("```\n");
            }
            CrossFileDiagnosticKind::ReactiveReferenceEscapes {
                variable_name,
                escaped_via,
                target_name,
            } => {
                append!(
                    *out,
                    "**Problem**: Reactive reference `{variable_name}` escapes its scope via {escaped_via}.\n\n",
                );
                if let Some(target) = target_name {
                    append!(*out, "**Escaped to**: `{target}`\n\n");
                }
                out.push_str("**Why this is implicit** (like Rust's move semantics):\n\n");
                out.push_str("```\n");
                out.push_str("β”Œβ”€ setup() ─────────────────────────────┐\n");
                append!(
                    *out,
                    "β”‚  const {variable_name} = reactive({{...}})          β”‚\n",
                );
                append!(
                    *out,
                    "β”‚  someFunction({variable_name})  ←── reference escapes β”‚\n",
                );
                out.push_str("β”‚          β”‚                              β”‚\n");
                out.push_str("β”‚          β–Ό                              β”‚\n");
                out.push_str("β”‚  β”Œβ”€ someFunction() ─────────────────┐  β”‚\n");
                append!(
                    *out,
                    "β”‚  β”‚  // {variable_name} is now accessible here    β”‚  β”‚\n",
                );
                out.push_str("β”‚  β”‚  // mutations affect original     β”‚  β”‚\n");
                out.push_str("β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚\n");
                out.push_str("β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜\n");
                out.push_str("```\n\n");
                out.push_str("**Issues**:\n\n");
                out.push_str("- πŸ” **Hidden Data Flow**: Mutations happen \"at a distance\" - hard to trace.\n");
                out.push_str(
                    "- πŸ› **Unexpected Side Effects**: Function may modify your reactive state.\n",
                );
                out.push_str(
                    "- πŸ“¦ **Ownership Unclear**: Who \"owns\" this reactive object now?\n\n",
                );
                out.push_str("**Explicit alternatives**:\n\n");
                out.push_str("```ts\n");
                out.push_str("// Option 1: Pass a readonly version\n");
                append!(*out, "someFunction(readonly({variable_name}))\n\n");
                out.push_str("// Option 2: Pass a snapshot (non-reactive copy)\n");
                append!(*out, "someFunction({{ ...{variable_name} }})\n\n");
                out.push_str("// Option 3: Pass specific values explicitly\n");
                append!(
                    *out,
                    "someFunction({variable_name}.id, {variable_name}.name)\n",
                );
                out.push_str("```\n");
            }
            CrossFileDiagnosticKind::ReactiveObjectMutatedAfterEscape {
                variable_name,
                mutation_site,
                escape_site,
            } => {
                append!(
                    *out,
                    "**Problem**: `{variable_name}` is mutated after escaping its scope.\n\n",
                );
                append!(*out, "- Escaped at offset: {escape_site}\n");
                append!(*out, "- Mutated at offset: {mutation_site}\n\n");
                out.push_str("**Timeline**:\n\n");
                out.push_str("```\n");
                append!(*out, "1. {variable_name} created in setup()\n");
                append!(
                    *out,
                    "2. {variable_name} passed to external function (escape)\n",
                );
                append!(
                    *out,
                    "3. {variable_name} mutated ← mutations may affect escaped reference!\n",
                );
                out.push_str("```\n\n");
                out.push_str("**This is similar to Rust's borrow checker**:\n\n");
                out.push_str("- In Rust: `cannot mutate while borrowed`\n");
                out.push_str("- In Vue: mutations after escape create implicit coupling\n\n");
                out.push_str("**Consider**: Document the mutation contract or use `readonly()`.\n");
            }
            CrossFileDiagnosticKind::CircularReactiveDependency { cycle } => {
                out.push_str("**Problem**: Circular reactive dependency detected.\n\n");
                out.push_str("**Dependency Cycle**:\n\n");
                out.push_str("```\n");
                for (i, node) in cycle.iter().enumerate() {
                    if i == 0 {
                        append!(*out, "β”Œβ”€β†’ {node}\n");
                    } else if i == cycle.len() - 1 {
                        append!(*out, "β”‚   ↓\n└── {node} β”€β”€β”€β”˜\n");
                    } else {
                        append!(*out, "β”‚   ↓\nβ”‚   {node}\n");
                    }
                }
                out.push_str("```\n\n");
                out.push_str("**Why this is dangerous**:\n\n");
                out.push_str("- πŸ’₯ **Infinite Update Loops**: Changes propagate endlessly.\n");
                out.push_str("- πŸ“š **Stack Overflow Risk**: Deep recursion in reactive updates.\n");
                out.push_str("- 🐌 **Performance Degradation**: Wasted computation cycles.\n\n");
                out.push_str("**How to fix**:\n\n");
                out.push_str("```ts\n");
                out.push_str("// Option 1: Use computed() to break the cycle\n");
                out.push_str("const derived = computed(() => {\n");
                out.push_str("  // Read without triggering write\n");
                out.push_str("  return transform(source.value)\n");
                out.push_str("})\n\n");
                out.push_str("// Option 2: Use watchEffect with explicit dependencies\n");
                out.push_str("watchEffect(() => {\n");
                out.push_str("  // One-way data flow only\n");
                out.push_str("})\n\n");
                out.push_str("// Option 3: Restructure to remove bidirectional dependency\n");
                out.push_str("```\n");
            }
            CrossFileDiagnosticKind::ProvideInjectWithoutSymbol { key, is_provide } => {
                let action = if *is_provide { "provide" } else { "inject" };
                append!(
                    *out,
                    "**Problem**: `{action}('{key}')` uses a string key instead of Symbol/InjectionKey.\n\n",
                );
                out.push_str("**Why string keys are problematic**:\n\n");
                out.push_str("```\n");
                out.push_str("β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”\n");
                out.push_str("β”‚  String Keys          β”‚  Symbol/InjectionKey           β”‚\n");
                out.push_str("β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€\n");
                out.push_str("β”‚  ❌ Name collisions    β”‚  βœ… Guaranteed uniqueness       β”‚\n");
                out.push_str("β”‚  ❌ No type safety     β”‚  βœ… Full TypeScript inference   β”‚\n");
                out.push_str("β”‚  ❌ Refactoring breaks β”‚  βœ… IDE rename support          β”‚\n");
                out.push_str("β”‚  ❌ Hard to trace      β”‚  βœ… Go-to-definition works      β”‚\n");
                out.push_str("β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜\n");
                out.push_str("```\n\n");
                out.push_str("**Name collision example**:\n\n");
                out.push_str("```ts\n");
                out.push_str("// ComponentA.vue\n");
                append!(*out, "provide('{key}', myData)\n\n");
                out.push_str("// LibraryX (unknown to you)\n");
                append!(*out, "provide('{key}', otherData)  // πŸ’₯ Collision!\n");
                out.push_str("```\n\n");
                out.push_str("**Type-safe pattern with InjectionKey**:\n\n");
                out.push_str("```ts\n");
                out.push_str("// injection-keys.ts\n");
                out.push_str("import type { InjectionKey, Ref } from 'vue'\n\n");
                out.push_str("export interface UserState {\n");
                out.push_str("  name: string\n");
                out.push_str("  id: number\n");
                out.push_str("}\n\n");
                out.push_str(
                    "export const UserKey: InjectionKey<Ref<UserState>> = Symbol('user')\n\n",
                );
                out.push_str("// Provider.vue\n");
                out.push_str("import { UserKey } from './injection-keys'\n");
                out.push_str("provide(UserKey, userData)  // βœ… Type-checked\n\n");
                out.push_str("// Consumer.vue\n");
                out.push_str("import { UserKey } from './injection-keys'\n");
                out.push_str(
                    "const user = inject(UserKey)  // βœ… Type: Ref<UserState> | undefined\n",
                );
                out.push_str("```\n");
            }
            CrossFileDiagnosticKind::WatchMutationCanBeComputed {
                watch_source,
                mutated_target,
                suggested_computed,
            } => {
                out.push_str("**Problem**: This `watch` callback only mutates a reactive value based on its source.\n\n");
                out.push_str("**Current code** (imperative, harder to trace):\n\n");
                out.push_str("```ts\n");
                append!(*out, "watch({watch_source}, (newVal) => {{\n");
                append!(*out, "  {mutated_target}.value = transform(newVal)\n");
                out.push_str("})\n");
                out.push_str("```\n\n");
                out.push_str("**Why `computed` is better**:\n\n");
                out.push_str("```\n");
                out.push_str("β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”\n");
                out.push_str("β”‚  watch + mutation       β”‚  computed                     β”‚\n");
                out.push_str("β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€\n");
                out.push_str("β”‚  ❌ Imperative flow      β”‚  βœ… Declarative transformation β”‚\n");
                out.push_str("β”‚  ❌ Two variables        β”‚  βœ… Single derived value       β”‚\n");
                out.push_str("β”‚  ❌ Manual sync needed   β”‚  βœ… Auto-cached and reactive   β”‚\n");
                out.push_str("β”‚  ❌ Side effects possibleβ”‚  βœ… Pure function guarantee    β”‚\n");
                out.push_str("β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜\n");
                out.push_str("```\n\n");
                out.push_str("**Refactored code** (declarative, easier to reason about):\n\n");
                out.push_str("```ts\n");
                append!(*out, "{suggested_computed}\n");
                out.push_str("```\n\n");
                out.push_str("**Note**: Use `watch` only when you need **side effects** (API calls, logging, etc.).\n");
            }
            CrossFileDiagnosticKind::DomAccessWithoutNextTick { api, context } => {
                append!(
                    *out,
                    "**Problem**: `{api}` is accessed in `{context}` without `nextTick()`.\n\n",
                );
                out.push_str("**Why this is dangerous**:\n\n");
                out.push_str("```\n");
                out.push_str("Timeline of Vue component lifecycle:\n");
                out.push_str("β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”\n");
                out.push_str("β”‚  1. setup() runs        β†’ DOM does NOT exist yet        β”‚\n");
                out.push_str("β”‚  2. Template renders    β†’ Virtual DOM created           β”‚\n");
                out.push_str("β”‚  3. onMounted() fires   β†’ DOM exists now                β”‚\n");
                out.push_str("β”‚  4. nextTick() resolves β†’ DOM is fully updated          β”‚\n");
                out.push_str("β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜\n");
                out.push_str("```\n\n");
                out.push_str("**SSR considerations**:\n\n");
                out.push_str("- On the server, `document` and `window` don't exist at all.\n");
                out.push_str(
                    "- Accessing them throws `ReferenceError: document is not defined`.\n\n",
                );
                out.push_str("**Safe patterns**:\n\n");
                out.push_str("```ts\n");
                out.push_str("// Option 1: Use inside onMounted\n");
                out.push_str("onMounted(() => {\n");
                append!(*out, "  {api}  // βœ… Safe - DOM exists\n");
                out.push_str("})\n\n");
                out.push_str("// Option 2: Use nextTick after state change\n");
                out.push_str("await nextTick()\n");
                append!(*out, "{api}  // βœ… Safe - DOM updated\n");
                out.push('\n');
                out.push_str("// Option 3: Guard for SSR\n");
                out.push_str("if (typeof document !== 'undefined') {\n");
                append!(*out, "  {api}  // βœ… Safe - browser only\n");
                out.push_str("}\n");
                out.push_str("```\n");
            }
            _ => {
                // Default: just show the message
            }
        }
    }
}