///|
/// Language-tool oriented analysis for `.mbtv` documents.
///|
/// Symbol surfaced to editors for outline views and quick navigation.
pub struct ToolingSymbol {
name : String
kind : String
span : @compiler.SourceSpan
} derive(Eq, Show)
///|
/// Aggregated semantic summary consumed by CLI tooling and the LSP layer.
pub struct ToolingAnalysis {
symbols : Array[ToolingSymbol]
expressions : Array[String]
diagnostics : Array[String]
island_components : Array[String]
extension : String
props_binding : String?
props : Array[String]
emits_binding : String?
emits : Array[String]
expose_binding : String?
expose : Array[String]
} derive(Eq, Show)
///|
/// Analyzer scoped to one source document.
priv struct ToolingAnalyzer {
source : String
filename : String
}
///|
/// Run a best-effort document analysis that never raises parser/compiler errors.
/// Failures are converted into `diagnostics` so editor features can degrade gently.
pub fn analyze(
source : String,
filename? : String = "Component.mbtv",
) -> ToolingAnalysis {
ToolingAnalyzer::new(source, filename).analyze()
}
///|
/// Create an analyzer scoped to one source document.
fn ToolingAnalyzer::new(source : String, filename : String) -> ToolingAnalyzer {
{ source, filename }
}
///|
/// Run a best-effort document analysis that never raises parser/compiler errors.
fn ToolingAnalyzer::analyze(self : ToolingAnalyzer) -> ToolingAnalysis {
match self.try_analyze() {
Some(analysis) => analysis
None =>
ToolingAnalyzer::failed_analysis("failed to analyze " + self.filename)
}
}
///|
/// Internal analysis pipeline that preserves partial results when parsing fails.
fn ToolingAnalyzer::try_analyze(self : ToolingAnalyzer) -> ToolingAnalysis? {
let doc = @compiler.parse_sfc(self.source, filename=self.filename) catch {
err => return Some(ToolingAnalyzer::failed_analysis(err.to_string()))
}
let template = @compiler.parse_template(doc.template) catch {
err =>
return Some(
ToolingAnalyzer::analysis_from_template_error(doc, err.to_string()),
)
}
Some(self.analysis_from_doc(doc, template))
}
///|
/// Default failure payload used when even a partial analysis cannot be produced.
fn ToolingAnalyzer::failed_analysis(message : String) -> ToolingAnalysis {
{
symbols: [],
expressions: [],
diagnostics: [message],
island_components: [],
extension: ".mbtv",
props_binding: None,
props: [],
emits_binding: None,
emits: [],
expose_binding: None,
expose: [],
}
}
///|
/// Build the degraded analysis used when template parsing fails after SFC parsing.
fn ToolingAnalyzer::analysis_from_template_error(
doc : @compiler.SfcDocument,
message : String,
) -> ToolingAnalysis {
{
symbols: ToolingAnalyzer::block_symbols(doc.blocks) +
ToolingAnalyzer::macro_symbols(doc),
expressions: [],
diagnostics: [message],
island_components: [],
extension: doc.extension,
props_binding: doc.props_binding,
props: ToolingAnalyzer::macro_names(doc.props),
emits_binding: doc.emits_binding,
emits: ToolingAnalyzer::macro_names(doc.emits),
expose_binding: doc.expose_binding,
expose: ToolingAnalyzer::macro_names(doc.expose),
}
}
///|
/// Build the full semantic summary once both SFC and template parsing succeed.
fn ToolingAnalyzer::analysis_from_doc(
self : ToolingAnalyzer,
doc : @compiler.SfcDocument,
template : Array[@compiler.TemplateNode],
) -> ToolingAnalysis {
let (expressions, island_components) = self.template_metadata(template)
{
symbols: ToolingAnalyzer::block_symbols(doc.blocks) +
ToolingAnalyzer::macro_symbols(doc),
expressions,
diagnostics: [],
island_components,
extension: doc.extension,
props_binding: doc.props_binding,
props: ToolingAnalyzer::macro_names(doc.props),
emits_binding: doc.emits_binding,
emits: ToolingAnalyzer::macro_names(doc.emits),
expose_binding: doc.expose_binding,
expose: ToolingAnalyzer::macro_names(doc.expose),
}
}
///|
/// Convert discovered SFC blocks into editor outline symbols.
fn ToolingAnalyzer::block_symbols(
blocks : Array[@compiler.SfcBlock],
) -> Array[ToolingSymbol] {
let symbols : Array[ToolingSymbol] = []
let mut style_index = 1
for block in blocks {
let (name, kind) = match block.kind {
@compiler_common.Template => ("template", "block")
@compiler_common.ScriptSetup => ("script", "block")
@compiler_common.ScriptExtern => ("script extern", "block")
@compiler_common.Style => {
let name = "style " + style_index.to_string()
style_index += 1
(name, "block")
}
}
symbols.push({ name, kind, span: block.span })
}
symbols
}
///|
/// Surface macro bindings such as `defineProps()` as synthetic symbols.
fn ToolingAnalyzer::macro_symbols(
doc : @compiler.SfcDocument,
) -> Array[ToolingSymbol] {
let symbols : Array[ToolingSymbol] = []
match doc.props_binding {
Some(name) =>
symbols.push({
name,
kind: "macro defineProps",
span: @compiler.new_source_span(0, 0),
})
None => ()
}
match doc.emits_binding {
Some(name) =>
symbols.push({
name,
kind: "macro defineEmits",
span: @compiler.new_source_span(0, 0),
})
None => ()
}
match doc.expose_binding {
Some(name) =>
symbols.push({
name,
kind: "macro defineExpose",
span: @compiler.new_source_span(0, 0),
})
None => ()
}
symbols
}
///|
/// Extract field names from compiler macro metadata.
fn ToolingAnalyzer::macro_names(
fields : Array[@compiler.MacroField],
) -> Array[String] {
let result : Array[String] = []
for field in fields {
result.push(field.name)
}
result
}
///|
/// Walk the template tree and collect expressions plus hydrated component tags.
fn ToolingAnalyzer::template_metadata(
self : ToolingAnalyzer,
template : Array[@compiler.TemplateNode],
) -> (Array[String], Array[String]) {
let expressions : Array[String] = []
let island_components : Array[String] = []
for node in template {
self.collect_template_metadata(node, expressions, island_components)
}
(expressions, island_components)
}
///|
/// Walk one template subtree and collect expressions plus hydrated component tags.
fn ToolingAnalyzer::collect_template_metadata(
self : ToolingAnalyzer,
node : @compiler.TemplateNode,
expressions : Array[String],
island_components : Array[String],
) -> Unit {
match node {
@compiler_common.Text(_) => ()
@compiler_common.Interpolation(expr) => expressions.push(expr)
@compiler_common.Element(element) => {
match element.if_expr {
Some(expr) => expressions.push(expr)
None => ()
}
match element.for_binding {
Some(binding) => expressions.push(binding.source)
None => ()
}
for attr in element.attrs {
match attr.kind {
@compiler_common.Dynamic | @compiler_common.Event =>
expressions.push(attr.value)
_ => ()
}
}
match element.island_mode {
@compiler_common.Load
| @compiler_common.Idle
| @compiler_common.Visible
| @compiler_common.Media(_)
| @compiler_common.Only
| @compiler_common.ServerDefer => island_components.push(element.tag)
@compiler_common.None => ()
}
for child in element.children {
self.collect_template_metadata(child, expressions, island_components)
}
}
@compiler_common.Conditional(branches) =>
for branch in branches {
match branch.condition {
Some(expr) => expressions.push(expr)
None => ()
}
self.collect_template_metadata(
@compiler_common.Element(branch.element),
expressions,
island_components,
)
}
@compiler_common.Match(node) =>
for branch in node.branches {
self.collect_template_metadata(
@compiler_common.Element(branch.element),
expressions,
island_components,
)
}
}
}