///|
/// Semantic index shared by hover, definition, completion, and diagnostics queries.
///|
/// Distinguishes direct macro access from accesses that go through `resolve_template_ref`.
priv enum ToolingBindingAccessMode {
Direct
ResolveTemplateRef
} derive(Eq)
///|
/// Definition site for one contract field that can appear in hover/definition results.
priv struct ToolingMemberDef {
contract_kind : String
binding_name : String
name : String
signature : String?
span : @compiler.SourceSpan
}
///|
/// Definition site for a binding such as `props`, `emit`, or a template ref.
priv struct ToolingBindingDef {
contract_kind : String
binding_name : String
span : @compiler.SourceSpan
fields : Array[@compiler.MacroField]
type_name : String?
access_mode : ToolingBindingAccessMode
}
///|
/// One resolved member access found in source code.
priv struct ToolingMemberRef {
contract_kind : String
binding_name : String
member_name : String
binding_span : @compiler.SourceSpan
member_span : @compiler.SourceSpan
}
///|
/// Full semantic index derived from a single `.mbtv` source document.
priv struct ToolingIndex {
document : ToolingDocument
diagnostics : Array[ToolingDiagnostic]
bindings : Array[ToolingBindingDef]
members : Array[ToolingMemberDef]
refs : Array[ToolingMemberRef]
}
///|
/// Mutable builder that accumulates definitions, references, and diagnostics.
priv struct ToolingIndexBuilder {
document : ToolingDocument
diagnostics : Array[ToolingDiagnostic]
bindings : Array[ToolingBindingDef]
members : Array[ToolingMemberDef]
refs : Array[ToolingMemberRef]
}
///|
/// Static target inferred from a named template ref in the template tree.
priv struct ToolingStaticTemplateRefTarget {
ref_name : String
type_name : String
fields : Array[@compiler.MacroField]
}
///|
/// Script-side template ref binding after its target type has been inferred.
priv struct ToolingResolvedTemplateRef {
binding_name : String
type_name : String
fields : Array[@compiler.MacroField]
}
///|
/// Build the semantic index consumed by editor queries and diagnostics.
fn build_tooling_index(source : String, filename : String) -> ToolingIndex {
ToolingIndexBuilder::new(source, filename).build()
}
///|
/// Create a fresh builder scoped to one source document.
fn ToolingIndexBuilder::new(
source : String,
filename : String,
) -> ToolingIndexBuilder {
{
document: ToolingDocument::new(source, filename),
diagnostics: [],
bindings: [],
members: [],
refs: [],
}
}
///|
/// Finalize the accumulated state into an immutable query index.
fn ToolingIndexBuilder::finish(self : ToolingIndexBuilder) -> ToolingIndex {
{
document: self.document,
diagnostics: self.diagnostics,
bindings: self.bindings,
members: self.members,
refs: self.refs,
}
}
///|
/// Build the semantic index consumed by editor queries and diagnostics.
fn ToolingIndexBuilder::build(self : ToolingIndexBuilder) -> ToolingIndex {
let source = self.document.source
let filename = self.document.filename
let doc = @compiler.parse_sfc(source, filename~) catch {
err => {
self.diagnostics.push({
severity: "error",
message: err.to_string(),
location: self.document.location_from_span({ start: 0, end: 0 }),
})
return self.finish()
}
}
let template = @compiler.parse_template(doc.template) catch {
err => {
self.diagnostics.push({
severity: "error",
message: err.to_string(),
location: self.document.location_from_span(
find_block_span(doc.blocks, @compiler_common.Template),
),
})
[]
}
}
let resolved_template_refs = resolve_tooling_template_refs(
doc, template, filename,
)
let script_span = self.document.find_block_content_span(
doc.blocks,
@compiler_common.ScriptSetup,
)
let template_span = self.document.find_block_content_span(
doc.blocks,
@compiler_common.Template,
)
self.collect_contract_index(
script_span,
"props",
doc.props_binding,
doc.props,
)
self.collect_contract_index(script_span, "emit", doc.emits_binding, doc.emits)
self.collect_contract_index(
script_span,
"slots",
doc.slots_binding,
doc.slots,
)
self.collect_contract_index(
script_span,
"expose",
doc.expose_binding,
doc.expose,
)
self.collect_template_ref_index(script_span, resolved_template_refs)
self.collect_contract_refs(script_span)
self.collect_contract_refs(template_span)
self.collect_template_ref_refs(script_span)
self.collect_template_ref_refs(template_span)
self.collect_semantic_diagnostics()
self.finish()
}
///|
/// Register one macro contract binding and all of its declared fields.
fn ToolingIndexBuilder::collect_contract_index(
self : ToolingIndexBuilder,
block_span : @compiler.SourceSpan?,
canonical_name : String,
binding_name : String?,
fields : Array[@compiler.MacroField],
) -> Unit {
match block_span {
Some(content_span) =>
match binding_name {
Some(binding) => {
let binding_span = self.document
.find_binding_definition_span(content_span, binding)
.unwrap_or(content_span)
self.bindings.push({
contract_kind: canonical_name,
binding_name: binding,
span: binding_span,
fields,
type_name: None,
access_mode: Direct,
})
for field in fields {
let span = self.document
.find_contract_field_definition_span(
content_span,
binding,
field.name,
)
.unwrap_or(binding_span)
self.members.push({
contract_kind: canonical_name,
binding_name: binding,
name: field.name,
signature: field.signature,
span,
})
}
}
None => ()
}
None => ()
}
}
///|
/// Register `use_template_ref()` bindings together with their inferred member sets.
fn ToolingIndexBuilder::collect_template_ref_index(
self : ToolingIndexBuilder,
block_span : @compiler.SourceSpan?,
resolved_template_refs : Array[ToolingResolvedTemplateRef],
) -> Unit {
match block_span {
Some(content_span) =>
for resolved_ref in resolved_template_refs {
let binding_span = self.document
.find_binding_definition_span(content_span, resolved_ref.binding_name)
.unwrap_or(content_span)
self.bindings.push({
contract_kind: "template ref",
binding_name: resolved_ref.binding_name,
span: binding_span,
fields: resolved_ref.fields,
type_name: Some(resolved_ref.type_name),
access_mode: ResolveTemplateRef,
})
for field in resolved_ref.fields {
self.members.push({
contract_kind: "template ref",
binding_name: resolved_ref.binding_name,
name: field.name,
signature: field.signature,
span: binding_span,
})
}
}
None => ()
}
}
///|
/// Scan source spans for member accesses against direct macro bindings.
fn ToolingIndexBuilder::collect_contract_refs(
self : ToolingIndexBuilder,
block_span : @compiler.SourceSpan?,
) -> Unit {
match block_span {
Some(span) => {
let candidates : Array[(String, String)] = []
for binding in self.bindings {
if binding.access_mode != Direct {
continue
}
candidates.push((binding.contract_kind, binding.binding_name))
let canonical = canonical_binding_name(binding.contract_kind)
if canonical != binding.binding_name {
candidates.push((binding.contract_kind, canonical))
}
}
for candidate in candidates {
for
ref_ in self.document.find_member_refs(span, candidate.0, candidate.1) {
if overlaps_definition(ref_, self.bindings, self.members) {
continue
}
self.refs.push(ref_)
}
}
}
None => ()
}
}
///|
/// Scan source spans for member accesses behind `resolve_template_ref(...)`.
fn ToolingIndexBuilder::collect_template_ref_refs(
self : ToolingIndexBuilder,
block_span : @compiler.SourceSpan?,
) -> Unit {
match block_span {
Some(span) =>
for binding in self.bindings {
if binding.access_mode != ResolveTemplateRef {
continue
}
for
ref_ in self.document.find_resolved_template_ref_member_refs(
span,
binding.binding_name,
) {
if overlaps_definition(ref_, self.bindings, self.members) {
continue
}
self.refs.push(ref_)
}
}
None => ()
}
}
///|
/// Emit semantic diagnostics such as unknown members or mismatched bindings.
fn ToolingIndexBuilder::collect_semantic_diagnostics(
self : ToolingIndexBuilder,
) -> Unit {
for ref_ in self.refs {
match find_binding(self.bindings, ref_.contract_kind, ref_.binding_name) {
Some(binding) =>
match
find_member(self.members, binding.binding_name, ref_.member_name) {
Some(_) => ()
None if binding.access_mode == ResolveTemplateRef &&
binding.fields.is_empty() => ()
None =>
self.diagnostics.push({
severity: "error",
message: unknown_member_message(ref_),
location: self.document.location_from_span(ref_.member_span),
})
}
None => {
let actual = find_binding_by_kind(self.bindings, ref_.contract_kind)
let message = match actual {
Some(binding) =>
"`" +
ref_.binding_name +
"` is not the " +
ref_.contract_kind +
" binding; use `" +
binding.binding_name +
"`"
None if ref_.contract_kind == "template ref" =>
"missing template ref binding for `" + ref_.binding_name + "`"
None =>
"missing define" +
uppercase_first(ref_.contract_kind) +
"() binding for `" +
ref_.binding_name +
"`"
}
self.diagnostics.push({
severity: "error",
message,
location: self.document.location_from_span(ref_.binding_span),
})
}
}
}
}
///|
/// Render the diagnostic shown for an unresolved member access.
fn unknown_member_message(ref_ : ToolingMemberRef) -> String {
if ref_.contract_kind == "emit" && ref_.member_name == "emit" {
"legacy `emit.emit(...)` is not supported; use typed event calls like `" +
ref_.binding_name +
".save()`"
} else if ref_.contract_kind == "template ref" {
"unknown template ref member `" + ref_.member_name + "`"
} else {
"unknown " + ref_.contract_kind + " member `" + ref_.member_name + "`"
}
}
///|
/// Infer template-ref binding types by combining script metadata with template refs.
fn resolve_tooling_template_refs(
doc : @compiler.SfcDocument,
nodes : Array[@compiler.TemplateNode],
filename : String,
) -> Array[ToolingResolvedTemplateRef] {
let static_targets = collect_static_template_ref_targets(nodes, filename)
let resolved : Array[ToolingResolvedTemplateRef] = []
for template_ref in doc.template_refs {
match template_ref.binding_name {
Some(binding_name) => {
let target = match template_ref.ref_name {
Some(name) =>
find_static_template_ref_target(static_targets, name).unwrap_or({
ref_name: name,
type_name: "@js_dom.Element",
fields: [],
})
None =>
{
ref_name: template_ref.arg_source,
type_name: "@js_dom.Element",
fields: [],
}
}
resolved.push({
binding_name,
type_name: target.type_name,
fields: target.fields,
})
}
None => ()
}
}
resolved
}
///|
/// Gather the static template refs declared in the template tree.
fn collect_static_template_ref_targets(
nodes : Array[@compiler.TemplateNode],
filename : String,
) -> Array[ToolingStaticTemplateRefTarget] {
let targets : Array[ToolingStaticTemplateRefTarget] = []
for node in nodes {
collect_static_template_ref_targets_into(node, filename, targets)
}
targets
}
///|
/// Walk one template subtree and register any statically named refs it contains.
fn collect_static_template_ref_targets_into(
node : @compiler.TemplateNode,
filename : String,
targets : Array[ToolingStaticTemplateRefTarget],
) -> Unit {
match node {
@compiler_common.Text(_) | @compiler_common.Interpolation(_) => ()
@compiler_common.Element(element) => {
match element.ref_static_name {
Some(name) =>
register_static_template_ref_target(
targets,
name,
infer_tooling_template_ref_target(element, filename),
)
None => ()
}
for child in element.children {
collect_static_template_ref_targets_into(child, filename, targets)
}
}
@compiler_common.Conditional(branches) =>
for branch in branches {
collect_static_template_ref_targets_into(
@compiler_common.Element(branch.element),
filename,
targets,
)
}
@compiler_common.Match(node) =>
for branch in node.branches {
collect_static_template_ref_targets_into(
@compiler_common.Element(branch.element),
filename,
targets,
)
}
}
}
///|
/// Merge a newly inferred target into the table keyed by ref name.
fn register_static_template_ref_target(
targets : Array[ToolingStaticTemplateRefTarget],
ref_name : String,
next_target : (String, Array[@compiler.MacroField]),
) -> Unit {
let next = { ref_name, type_name: next_target.0, fields: next_target.1 }
for i in 0.. ToolingStaticTemplateRefTarget {
if current.type_name == next_.type_name &&
macro_fields_equal(current.fields, next_.fields) {
return current
}
{ ref_name: current.ref_name, type_name: "@js_dom.Element", fields: [] }
}
///|
/// Compare macro field lists exactly so ref targets can be merged safely.
fn macro_fields_equal(
left : Array[@compiler.MacroField],
right : Array[@compiler.MacroField],
) -> Bool {
if left.length() != right.length() {
return false
}
for i in 0.. (String, Array[@compiler.MacroField]) {
if element.is_component {
return infer_component_template_ref_target(element.tag, filename)
}
(infer_native_template_ref_type_name(element.tag), [])
}
///|
/// Resolve component refs by reading the referenced component file when possible.
fn infer_component_template_ref_target(
tag : String,
filename : String,
) -> (String, Array[@compiler.MacroField]) {
for path in component_template_ref_candidate_paths(filename, tag) {
if not(@fs.path_exists(path)) {
continue
}
let source = @fs.read_file_to_string(path) catch { _ => continue }
let doc = @compiler.parse_sfc(source, filename=path) catch { _ => continue }
if not(doc.expose.is_empty()) || doc.expose_binding is Some(_) {
return (tag + " expose", doc.expose)
}
return ("component " + tag, [])
}
("component " + tag, [])
}
///|
fn component_template_ref_candidate_paths(
filename : String,
tag : String,
) -> Array[String] {
let candidates : Array[String] = []
let dir = path_dirname(filename)
push_unique_string(candidates, join_path(dir, tag + ".mbtv"))
push_unique_string(candidates, join_path(dir, kebab_case_tag(tag) + ".mbtv"))
candidates
}
///|
fn find_static_template_ref_target(
targets : Array[ToolingStaticTemplateRefTarget],
ref_name : String,
) -> ToolingStaticTemplateRefTarget? {
for target in targets {
if target.ref_name == ref_name {
return Some(target)
}
}
None
}
///|
fn infer_native_template_ref_type_name(tag : String) -> String {
match tag {
"a" => "@js_dom.HTMLAnchorElement"
"body" => "@js_dom.HTMLBodyElement"
"button" => "@js_dom.HTMLButtonElement"
"canvas" => "@js_dom.HTMLCanvasElement"
"div" => "@js_dom.HTMLDivElement"
"form" => "@js_dom.HTMLFormElement"
"h1" | "h2" | "h3" | "h4" | "h5" | "h6" => "@js_dom.HTMLHeadingElement"
"head" => "@js_dom.HTMLHeadElement"
"html" => "@js_dom.HTMLHtmlElement"
"iframe" => "@js_dom.HTMLIFrameElement"
"img" => "@js_dom.HTMLImageElement"
"input" => "@js_dom.HTMLInputElement"
"label" => "@js_dom.HTMLLabelElement"
"li" => "@js_dom.HTMLLIElement"
"ol" => "@js_dom.HTMLOListElement"
"option" => "@js_dom.HTMLOptionElement"
"p" => "@js_dom.HTMLParagraphElement"
"select" => "@js_dom.HTMLSelectElement"
"span" => "@js_dom.HTMLSpanElement"
"textarea" => "@js_dom.HTMLTextAreaElement"
"ul" => "@js_dom.HTMLUListElement"
_ => "@js_dom.HTMLElement"
}
}
///|
fn path_dirname(path : String) -> String {
let chars = path.to_array()
let mut cursor = chars.length()
while cursor > 0 {
let ch = chars[cursor - 1]
if ch == '/' || ch == '\\' {
return String::unsafe_substring(path, start=0, end=cursor - 1)
}
cursor -= 1
}
""
}
///|
fn join_path(dir : String, base : String) -> String {
if dir.length() == 0 {
return base
}
dir + "/" + base
}
///|
fn kebab_case_tag(tag : String) -> String {
let sb = StringBuilder::new()
let chars = tag.to_array()
for i in 0..= 'A' && ch <= 'Z' {
if i > 0 {
let prev = chars[i - 1]
if (prev >= 'a' && prev <= 'z') || (prev >= '0' && prev <= '9') {
sb.write_char('-')
}
}
sb.write_char(
(ch.to_int() - 'A'.to_int() + 'a'.to_int()).unsafe_to_char(),
)
} else {
sb.write_char(ch)
}
}
sb.to_string()
}
///|
fn push_unique_string(values : Array[String], next_ : String) -> Unit {
for value in values {
if value == next_ {
return
}
}
values.push(next_)
}