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
//! Component resolution analyzer.
//!
//! Detects unregistered components and unresolved imports.
use crate::cross_file::diagnostics::{
CrossFileDiagnostic, CrossFileDiagnosticKind, DiagnosticSeverity,
};
use crate::cross_file::graph::DependencyGraph;
use crate::cross_file::registry::{FileId, ModuleRegistry};
use vize_carton::{cstr, CompactString, FxHashSet};
/// Information about a component resolution issue.
#[derive(Debug, Clone)]
pub struct ComponentResolutionIssue {
/// The file where the issue was found.
pub file_id: FileId,
/// The component name or import specifier.
pub name: CompactString,
/// Kind of issue.
pub kind: ComponentResolutionIssueKind,
/// Source offset.
pub offset: u32,
}
/// Kind of component resolution issue.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ComponentResolutionIssueKind {
/// Component used in template but not imported/registered.
UnregisteredComponent,
/// Import specifier could not be resolved.
UnresolvedImport,
}
/// Analyze component resolution across all files.
///
/// This analyzer checks:
/// 1. All components used in templates are properly imported/registered
/// 2. All import specifiers can be resolved to actual files
pub fn analyze_component_resolution(
registry: &ModuleRegistry,
graph: &DependencyGraph,
) -> (Vec<ComponentResolutionIssue>, Vec<CrossFileDiagnostic>) {
let mut issues = Vec::new();
let mut diagnostics = Vec::new();
// Build a set of all registered component names from the dependency graph
let registered_components: FxHashSet<&str> = graph
.nodes()
.filter_map(|node| node.component_name.as_deref())
.collect();
// Check each file
for entry in registry.iter() {
let file_id = entry.id;
let analysis = &entry.analysis;
// Get all imported identifiers from this file
let imported_identifiers: FxHashSet<&str> = analysis
.scopes
.iter()
.flat_map(|scope| scope.bindings().map(|(name, _)| name))
.collect();
// Check used components
for component_name in &analysis.used_components {
// Skip built-in components
if is_builtin_component(component_name.as_str()) {
continue;
}
// Check if component is imported as a binding
let is_imported = imported_identifiers.contains(component_name.as_str());
// Check if component exists in the project (registered in graph)
let exists_in_project = registered_components.contains(component_name.as_str());
// Check if it's available as a global component name (via import)
let is_available = is_imported
|| exists_in_project
|| analysis.bindings.contains(component_name.as_str());
if !is_available {
let issue = ComponentResolutionIssue {
file_id,
name: component_name.clone(),
kind: ComponentResolutionIssueKind::UnregisteredComponent,
offset: 0, // TODO: Get actual offset from template
};
issues.push(issue);
let diagnostic = CrossFileDiagnostic::new(
CrossFileDiagnosticKind::UnregisteredComponent {
component_name: component_name.clone(),
template_offset: 0,
},
DiagnosticSeverity::Error,
file_id,
0,
cstr!(
"**Unregistered Component**: `<{}>` is used in template but not imported\n\n\
The component must be imported in `<script setup>` or registered globally.",
component_name
),
)
.with_suggestion(cstr!(
"```typescript\nimport {} from './{}.vue'\n```",
component_name, component_name
));
diagnostics.push(diagnostic);
}
}
// Check for unresolved imports
for scope in analysis.scopes.iter() {
if scope.kind == crate::scope::ScopeKind::ExternalModule {
if let crate::scope::ScopeData::ExternalModule(data) = scope.data() {
let source = &data.source;
// Skip node_modules imports (bare specifiers)
if !source.starts_with('.')
&& !source.starts_with('/')
&& !source.starts_with('@')
{
continue;
}
// Skip @-prefixed imports that are likely aliases
if source.starts_with('@') && !source.starts_with("@/") {
continue;
}
// Check if the import resolves to a known file
let resolved = resolve_import(source, registry, entry.path.parent());
if !resolved {
let issue = ComponentResolutionIssue {
file_id,
name: source.clone(),
kind: ComponentResolutionIssueKind::UnresolvedImport,
offset: scope.span.start,
};
issues.push(issue);
let diagnostic = CrossFileDiagnostic::new(
CrossFileDiagnosticKind::UnresolvedImport {
specifier: source.clone(),
import_offset: scope.span.start,
},
DiagnosticSeverity::Error,
file_id,
scope.span.start,
cstr!(
"**Unresolved Import**: Cannot find module `{}`\n\n\
- Check if the file exists at the specified path\n\
- Verify the import path is correct (relative paths start with `./` or `../`)\n\
- For alias imports like `@/`, ensure tsconfig paths are configured",
source
),
);
diagnostics.push(diagnostic);
}
}
}
}
}
(issues, diagnostics)
}
/// Check if a component name is a Vue built-in component.
#[inline]
fn is_builtin_component(name: &str) -> bool {
matches!(
name,
"Transition"
| "TransitionGroup"
| "KeepAlive"
| "Suspense"
| "Teleport"
| "component"
| "slot"
| "template"
// Nuxt built-ins
| "NuxtPage"
| "NuxtLayout"
| "NuxtLink"
| "NuxtLoadingIndicator"
| "NuxtErrorBoundary"
| "NuxtWelcome"
| "NuxtIsland"
| "ClientOnly"
| "DevOnly"
| "ServerPlaceholder"
// Vue Router
| "RouterView"
| "RouterLink"
// Head management
| "Head"
| "Html"
| "Body"
| "Title"
| "Meta"
| "Style"
| "Link"
| "Base"
| "NoScript"
| "Script"
)
}
/// Try to resolve an import specifier to a file in the registry.
#[allow(clippy::disallowed_macros)]
fn resolve_import(
specifier: &str,
registry: &ModuleRegistry,
from_dir: Option<&std::path::Path>,
) -> bool {
// Handle @/ alias (common Vue project alias)
if let Some(relative) = specifier.strip_prefix("@/") {
// Check with common extensions
for ext in &["", ".vue", ".ts", ".tsx", ".js", ".jsx"] {
let path = format!("src/{}{}", relative, ext);
if registry.get_by_path(&path).is_some() {
return true;
}
}
return false;
}
// Handle relative imports
if specifier.starts_with('.') {
// First, try to resolve using the directory path
if let Some(dir) = from_dir {
// Try with common extensions
for ext in &[
"",
".vue",
".ts",
".tsx",
".js",
".jsx",
"/index.ts",
"/index.vue",
] {
let resolved = dir.join(format!("{}{}", specifier, ext));
if registry.get_by_path(&resolved).is_some() {
return true;
}
}
}
// Fallback: try to match by filename only (for flat file structures like playground presets)
// Extract the filename from the specifier (e.g., "./ChildComponent.vue" -> "ChildComponent.vue")
let filename = specifier
.strip_prefix("./")
.or_else(|| specifier.strip_prefix("../"))
.unwrap_or(specifier);
// Try with common extensions if no extension is provided
let extensions = if filename.contains('.') {
vec![""]
} else {
vec![".vue", ".ts", ".tsx", ".js", ".jsx", ""]
};
for ext in extensions {
let target = format!("{}{}", filename, ext);
// Check if any file in the registry ends with this filename
for entry in registry.iter() {
let entry_path = entry.path.to_string_lossy();
if entry_path.ends_with(&target) || entry_path == target {
return true;
}
}
}
return false;
}
// For absolute or other paths, check directly
registry.get_by_path(specifier).is_some()
}
#[cfg(test)]
mod tests {
use super::is_builtin_component;
#[test]
fn test_is_builtin_component() {
assert!(is_builtin_component("Transition"));
assert!(is_builtin_component("KeepAlive"));
assert!(is_builtin_component("RouterView"));
assert!(is_builtin_component("NuxtPage"));
assert!(!is_builtin_component("MyComponent"));
assert!(!is_builtin_component("UserCard"));
}
}