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
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
//! Cross-file diagnostic types.
//!
//! Diagnostics produced by cross-file analysis that span multiple files.
//!
//! This module is split into:
//! - Core types and constructors (this file)
//! - [`rules`]: Diagnostic code identifiers for filtering/configuration
//! - [`formatting`]: Rich Markdown rendering of diagnostics
mod formatting;
mod rules;
use super::registry::FileId;
use vize_carton::CompactString;
/// Severity level of a diagnostic.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u8)]
pub enum DiagnosticSeverity {
/// Error - must be fixed.
Error = 0,
/// Warning - should be addressed.
Warning = 1,
/// Information - for awareness.
Info = 2,
/// Hint - suggestion for improvement.
Hint = 3,
}
impl DiagnosticSeverity {
/// Get display name.
#[inline]
pub const fn display_name(&self) -> &'static str {
match self {
Self::Error => "error",
Self::Warning => "warning",
Self::Info => "info",
Self::Hint => "hint",
}
}
}
/// Kind of cross-file diagnostic.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CrossFileDiagnosticKind {
// === Fallthrough Attributes ===
/// Component doesn't use $attrs but parent passes attributes.
UnusedFallthroughAttrs { passed_attrs: Vec<CompactString> },
/// `inheritAttrs: false` but $attrs not explicitly bound.
InheritAttrsDisabledUnused,
/// Multiple root elements without explicit v-bind="$attrs".
MultiRootMissingAttrs,
// === Component Emits ===
/// Emit called but not declared in defineEmits.
UndeclaredEmit { emit_name: CompactString },
/// Declared emit is never called.
UnusedEmit { emit_name: CompactString },
/// Parent listens for event not emitted by child.
UnmatchedEventListener { event_name: CompactString },
// === Event Bubbling ===
/// Event emitted but no ancestor handles it.
UnhandledEvent {
event_name: CompactString,
depth: usize,
},
/// Event handler modifiers may cause issues (.stop, .prevent).
EventModifierIssue {
event_name: CompactString,
modifier: CompactString,
},
// === Provide/Inject ===
/// inject() key has no matching provide() in ancestors.
UnmatchedInject { key: CompactString },
/// provide() key is never injected by descendants.
UnusedProvide { key: CompactString },
/// Type mismatch between provide and inject.
ProvideInjectTypeMismatch {
key: CompactString,
provided_type: CompactString,
injected_type: CompactString,
},
/// provide/inject uses string key instead of Symbol/InjectionKey.
/// String keys lack type safety and can collide.
ProvideInjectWithoutSymbol {
key: CompactString,
is_provide: bool,
},
// === Unique Element IDs ===
/// Duplicate ID attribute across components.
DuplicateElementId {
id: CompactString,
locations: Vec<(FileId, u32)>,
},
/// ID generated in v-for may not be unique.
NonUniqueIdInLoop { id_expression: CompactString },
// === Server/Client Boundary ===
/// Browser API used in potentially SSR context.
BrowserApiInSsr {
api: CompactString,
context: CompactString,
},
/// Async component not wrapped in Suspense.
AsyncWithoutSuspense { component_name: CompactString },
/// Hydration mismatch risk (client-only content).
HydrationMismatchRisk { reason: CompactString },
// === Error/Suspense Boundaries ===
/// Error thrown but no onErrorCaptured in ancestors.
UncaughtErrorBoundary,
/// Async operation without Suspense boundary.
MissingSuspenseBoundary,
/// Nested Suspense without fallback.
SuspenseWithoutFallback,
// === Dependency Graph ===
/// Circular dependency detected.
CircularDependency { cycle: Vec<CompactString> },
/// Deep import chain (performance concern).
DeepImportChain {
depth: usize,
chain: Vec<CompactString>,
},
// === Component Resolution (Static Analysis) ===
/// Component used in template but not imported/registered.
UnregisteredComponent {
component_name: CompactString,
template_offset: u32,
},
/// Import specifier could not be resolved to a file.
UnresolvedImport {
specifier: CompactString,
import_offset: u32,
},
// === Props Validation (Static Analysis) ===
/// Prop passed to component but not declared in child's defineProps.
UndeclaredProp {
prop_name: CompactString,
component_name: CompactString,
},
/// Required prop not passed to component.
MissingRequiredProp {
prop_name: CompactString,
component_name: CompactString,
},
/// Prop type mismatch (literal type check).
PropTypeMismatch {
prop_name: CompactString,
expected_type: CompactString,
actual_type: CompactString,
},
// === Slot Validation (Static Analysis) ===
/// Slot used but not defined in child component's defineSlots.
UndefinedSlot {
slot_name: CompactString,
component_name: CompactString,
},
// === Setup Context Violations ===
/// Reactivity API (ref, reactive, computed) called outside setup context.
/// This can cause CSRP (Client-Side Rendering Problems) and state pollution.
ReactivityOutsideSetup {
api_name: CompactString,
context_description: CompactString,
},
/// Lifecycle hook called outside setup context.
/// These hooks must be called synchronously during setup.
LifecycleOutsideSetup {
hook_name: CompactString,
context_description: CompactString,
},
/// Watcher (watch, watchEffect) called outside setup context.
/// This can cause memory leaks as the watcher won't be automatically cleaned up.
WatcherOutsideSetup {
api_name: CompactString,
context_description: CompactString,
},
/// Dependency injection (provide, inject) called outside setup context.
/// These must be called during component setup.
DependencyInjectionOutsideSetup {
api_name: CompactString,
context_description: CompactString,
},
/// Composable function called outside setup context.
/// Composables that use Vue APIs must be called within setup.
ComposableOutsideSetup {
composable_name: CompactString,
context_description: CompactString,
},
// === Reactivity Reference Loss ===
/// Spread operator used on reactive object, breaking reactivity.
/// `const copy = { ...reactive }` creates a non-reactive shallow copy.
SpreadBreaksReactivity {
source_name: CompactString,
source_type: CompactString, // "reactive" | "ref" | "props"
},
/// Reactive variable reassigned, breaking reactivity reference.
/// `let r = ref(0); r = ref(1)` loses the original ref.
ReassignmentBreaksReactivity {
variable_name: CompactString,
original_type: CompactString,
},
/// Reactive value extracted to plain variable, breaking reactivity.
/// `const count = ref(0).value` loses reactivity.
ValueExtractionBreaksReactivity {
source_name: CompactString,
extracted_value: CompactString,
},
/// Destructuring reactive object/props without toRefs, breaking reactivity.
/// `const { count } = props` loses reactivity.
DestructuringBreaksReactivity {
source_name: CompactString,
destructured_keys: Vec<CompactString>,
suggestion: CompactString, // "toRefs" | "toRef" | "storeToRefs"
},
/// Reactive reference escapes scope implicitly via function parameter.
/// This makes the data flow implicit and harder to trace.
ReactiveReferenceEscapes {
variable_name: CompactString,
escaped_via: CompactString, // "function call" | "return" | "assignment to outer scope"
target_name: Option<CompactString>, // function name or variable name if known
},
/// Reactive object mutated after being passed to external function.
/// This can cause unexpected side effects.
ReactiveObjectMutatedAfterEscape {
variable_name: CompactString,
mutation_site: u32,
escape_site: u32,
},
/// Circular reactive dependency detected.
/// This can cause infinite update loops or stack overflow.
CircularReactiveDependency { cycle: Vec<CompactString> },
/// Watch callback that only mutates a reactive value could be computed.
/// `watch(a, () => { b.value = transform(a.value) })` → `const b = computed(() => transform(a.value))`
WatchMutationCanBeComputed {
watch_source: CompactString,
mutated_target: CompactString,
suggested_computed: CompactString,
},
/// DOM API (document, window) accessed outside of lifecycle hooks or nextTick.
/// In SSR or before mount, the DOM doesn't exist yet.
DomAccessWithoutNextTick {
api: CompactString,
context: CompactString, // "setup" | "computed" | "watch callback"
},
// === Ultra-Strict Diagnostics (Rust-like paranoia) ===
/// Computed property contains side effects (mutations, console.log, API calls).
/// Computed should be pure functions - side effects make them unpredictable.
ComputedHasSideEffects {
computed_name: CompactString,
side_effect: CompactString, // "mutation" | "console" | "fetch" | "assignment"
},
/// Reactive state declared at module scope risks Cross-request State Pollution (CSRP).
/// In SSR, module-level state is shared across all requests.
ReactiveStateAtModuleScope {
variable_name: CompactString,
reactive_type: CompactString, // "ref" | "reactive" | "computed"
},
/// Template ref is accessed during setup (before it's populated).
/// Template refs are `null` until the component is mounted.
TemplateRefAccessedBeforeMount {
ref_name: CompactString,
access_context: CompactString, // "setup" | "computed" | "watchEffect"
},
/// Reactive state accessed across an async boundary without proper handling.
/// The component may have unmounted or the value changed before await returns.
AsyncBoundaryCrossing {
variable_name: CompactString,
async_context: CompactString, // "await" | "setTimeout" | "promise callback"
},
/// Closure captures reactive state implicitly.
/// Like Rust's closure capture, this creates hidden dependencies.
ClosureCapturesReactive {
closure_context: CompactString,
captured_variables: Vec<CompactString>,
},
/// Object identity comparison (===) on reactive objects.
/// Reactive proxies have different identity than raw objects.
ObjectIdentityComparison {
left_operand: CompactString,
right_operand: CompactString,
},
/// Reactive state is exported from module, creating global mutable state.
/// This violates encapsulation and makes state flow hard to trace.
ReactiveStateExported {
variable_name: CompactString,
export_type: CompactString, // "named" | "default" | "re-export"
},
/// Deep access on shallowRef/shallowReactive bypasses reactivity.
/// Changes to nested properties won't trigger updates.
ShallowReactiveDeepAccess {
variable_name: CompactString,
access_path: CompactString, // "value.nested.prop"
},
/// toRaw() value is mutated, bypassing reactivity entirely.
/// Mutations to raw values don't trigger reactive updates.
ToRawMutation {
original_variable: CompactString,
mutation_type: CompactString, // "property assignment" | "method call"
},
/// Event listener added without corresponding cleanup.
/// This causes memory leaks if the component is destroyed.
EventListenerWithoutCleanup {
event_name: CompactString,
target: CompactString, // "document" | "window" | "element"
},
/// Reactive array mutated with non-triggering method.
/// Some array methods don't trigger reactive updates.
ArrayMutationNotTriggering {
array_name: CompactString,
method: CompactString, // "sort" | "reverse" | "fill" direct assignment
},
/// Store getter accessed in setup without storeToRefs.
/// Pinia getters need storeToRefs() to maintain reactivity.
PiniaGetterWithoutStoreToRefs {
store_name: CompactString,
getter_name: CompactString,
},
/// watchEffect callback contains async operations.
/// Async operations in watchEffect can cause race conditions.
WatchEffectWithAsync {
async_operation: CompactString, // "await" | "setTimeout" | "fetch"
},
// === Unified Setup Context Violation ===
/// Vue API called outside of setup context (module-level in non-setup script).
/// Wraps SetupContextViolationKind for unified handling.
SetupContextViolation {
kind: crate::setup_context::SetupContextViolationKind,
api_name: CompactString,
},
}
/// A cross-file diagnostic with location information.
#[derive(Debug, Clone)]
pub struct CrossFileDiagnostic {
/// Diagnostic kind.
pub kind: CrossFileDiagnosticKind,
/// Severity level.
pub severity: DiagnosticSeverity,
/// Primary file where the issue originates.
pub primary_file: FileId,
/// Start offset in the primary file.
pub primary_offset: u32,
/// End offset in the primary file (for highlighting range).
pub primary_end_offset: u32,
/// Related files involved in this diagnostic.
pub related_files: Vec<(FileId, u32, CompactString)>,
/// Human-readable message.
pub message: CompactString,
/// Optional fix suggestion.
pub suggestion: Option<CompactString>,
}
impl CrossFileDiagnostic {
/// Create a new diagnostic.
pub fn new(
kind: CrossFileDiagnosticKind,
severity: DiagnosticSeverity,
primary_file: FileId,
primary_offset: u32,
message: impl Into<CompactString>,
) -> Self {
Self {
kind,
severity,
primary_file,
primary_offset,
primary_end_offset: primary_offset, // Default to same as start
related_files: Vec::new(),
message: message.into(),
suggestion: None,
}
}
/// Create a new diagnostic with span (start and end offset).
pub fn with_span(
kind: CrossFileDiagnosticKind,
severity: DiagnosticSeverity,
primary_file: FileId,
primary_offset: u32,
primary_end_offset: u32,
message: impl Into<CompactString>,
) -> Self {
Self {
kind,
severity,
primary_file,
primary_offset,
primary_end_offset,
related_files: Vec::new(),
message: message.into(),
suggestion: None,
}
}
/// Set the end offset for the diagnostic span.
pub fn with_end_offset(mut self, end_offset: u32) -> Self {
self.primary_end_offset = end_offset;
self
}
/// Add a related file location.
pub fn with_related(
mut self,
file: FileId,
offset: u32,
description: impl Into<CompactString>,
) -> Self {
self.related_files.push((file, offset, description.into()));
self
}
/// Add a fix suggestion.
pub fn with_suggestion(mut self, suggestion: impl Into<CompactString>) -> Self {
self.suggestion = Some(suggestion.into());
self
}
/// Check if this is an error.
#[inline]
pub fn is_error(&self) -> bool {
self.severity == DiagnosticSeverity::Error
}
/// Check if this is a warning.
#[inline]
pub fn is_warning(&self) -> bool {
self.severity == DiagnosticSeverity::Warning
}
}
#[cfg(test)]
#[path = "diagnostics_tests.rs"]
mod tests;