#![allow(dead_code)]
use crate::common::Span;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u8)]
pub enum VueApiKind {
Provide = 0,
Inject = 1,
Ref = 2,
Reactive = 3,
Computed = 4,
ShallowRef = 5,
ShallowReactive = 6,
Watch = 7,
WatchEffect = 8,
WatchPostEffect = 9,
WatchSyncEffect = 10,
OnMounted = 11,
OnUnmounted = 12,
OnBeforeMount = 13,
OnBeforeUnmount = 14,
OnUpdated = 15,
OnBeforeUpdate = 16,
OnErrorCaptured = 17,
OnActivated = 18,
OnDeactivated = 19,
OnRenderTracked = 20,
OnRenderTriggered = 21,
OnServerPrefetch = 22,
UseSlots = 23,
UseAttrs = 24,
UseTemplateRef = 25,
GetCurrentInstance = 26,
}
impl VueApiKind {
pub const fn category(&self) -> VueApiCategory {
match self {
Self::Provide | Self::Inject => VueApiCategory::DependencyInjection,
Self::Ref
| Self::Reactive
| Self::Computed
| Self::ShallowRef
| Self::ShallowReactive => VueApiCategory::Reactivity,
Self::Watch | Self::WatchEffect | Self::WatchPostEffect | Self::WatchSyncEffect => {
VueApiCategory::Watchers
}
Self::OnMounted
| Self::OnUnmounted
| Self::OnBeforeMount
| Self::OnBeforeUnmount
| Self::OnUpdated
| Self::OnBeforeUpdate
| Self::OnErrorCaptured
| Self::OnActivated
| Self::OnDeactivated
| Self::OnRenderTracked
| Self::OnRenderTriggered
| Self::OnServerPrefetch => VueApiCategory::Lifecycle,
Self::UseSlots | Self::UseAttrs | Self::UseTemplateRef => VueApiCategory::TemplateUtils,
Self::GetCurrentInstance => VueApiCategory::InstanceAccess,
}
}
pub const fn description(&self) -> &'static str {
match self {
Self::Provide => "provide() dependency injection",
Self::Inject => "inject() dependency injection",
Self::Ref => "ref() reactive reference",
Self::Reactive => "reactive() reactive object",
Self::Computed => "computed() computed property",
Self::ShallowRef => "shallowRef() shallow reactive reference",
Self::ShallowReactive => "shallowReactive() shallow reactive object",
Self::Watch => "watch() watcher",
Self::WatchEffect => "watchEffect() effect watcher",
Self::WatchPostEffect => "watchPostEffect() post-render effect",
Self::WatchSyncEffect => "watchSyncEffect() synchronous effect",
Self::OnMounted => "onMounted() lifecycle hook",
Self::OnUnmounted => "onUnmounted() lifecycle hook",
Self::OnBeforeMount => "onBeforeMount() lifecycle hook",
Self::OnBeforeUnmount => "onBeforeUnmount() lifecycle hook",
Self::OnUpdated => "onUpdated() lifecycle hook",
Self::OnBeforeUpdate => "onBeforeUpdate() lifecycle hook",
Self::OnErrorCaptured => "onErrorCaptured() lifecycle hook",
Self::OnActivated => "onActivated() keep-alive hook",
Self::OnDeactivated => "onDeactivated() keep-alive hook",
Self::OnRenderTracked => "onRenderTracked() debug hook",
Self::OnRenderTriggered => "onRenderTriggered() debug hook",
Self::OnServerPrefetch => "onServerPrefetch() SSR hook",
Self::UseSlots => "useSlots() slot access",
Self::UseAttrs => "useAttrs() attribute access",
Self::UseTemplateRef => "useTemplateRef() template ref access",
Self::GetCurrentInstance => {
"getCurrentInstance() instance access (sync context required)"
}
}
}
pub const fn requires_sync_context(&self) -> bool {
matches!(self, Self::GetCurrentInstance)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum VueApiCategory {
DependencyInjection,
Reactivity,
Watchers,
Lifecycle,
TemplateUtils,
InstanceAccess,
}
#[inline]
pub fn detect_vue_api_call(name: &[u8]) -> Option<VueApiKind> {
let len = name.len();
if !(3..=18).contains(&len) {
return None;
}
match name.first()? {
b'p' => detect_p_api(name, len),
b'i' => detect_i_api(name, len),
b'r' => detect_r_api(name, len),
b'c' => detect_c_api(name, len),
b's' => detect_s_api(name, len),
b'w' => detect_w_api(name, len),
b'o' => detect_o_api(name, len),
b'u' => detect_u_api(name, len),
b'g' => detect_g_api(name, len),
_ => None,
}
}
fn detect_p_api(name: &[u8], len: usize) -> Option<VueApiKind> {
if len == 7 && name == b"provide" {
Some(VueApiKind::Provide)
} else {
None
}
}
fn detect_i_api(name: &[u8], len: usize) -> Option<VueApiKind> {
if len == 6 && name == b"inject" {
Some(VueApiKind::Inject)
} else {
None
}
}
fn detect_r_api(name: &[u8], len: usize) -> Option<VueApiKind> {
match len {
3 if name == b"ref" => Some(VueApiKind::Ref),
8 if name == b"reactive" => Some(VueApiKind::Reactive),
_ => None,
}
}
fn detect_c_api(name: &[u8], len: usize) -> Option<VueApiKind> {
if len == 8 && name == b"computed" {
Some(VueApiKind::Computed)
} else {
None
}
}
fn detect_s_api(name: &[u8], len: usize) -> Option<VueApiKind> {
match len {
10 if name == b"shallowRef" => Some(VueApiKind::ShallowRef),
15 if name == b"shallowReactive" => Some(VueApiKind::ShallowReactive),
_ => None,
}
}
fn detect_w_api(name: &[u8], len: usize) -> Option<VueApiKind> {
match len {
5 if name == b"watch" => Some(VueApiKind::Watch),
11 if name == b"watchEffect" => Some(VueApiKind::WatchEffect),
15 if name == b"watchPostEffect" => Some(VueApiKind::WatchPostEffect),
15 if name == b"watchSyncEffect" => Some(VueApiKind::WatchSyncEffect),
_ => None,
}
}
fn detect_o_api(name: &[u8], len: usize) -> Option<VueApiKind> {
match len {
9 => {
if name == b"onMounted" {
Some(VueApiKind::OnMounted)
} else if name == b"onUpdated" {
Some(VueApiKind::OnUpdated)
} else {
None
}
}
11 => {
if name == b"onUnmounted" {
Some(VueApiKind::OnUnmounted)
} else if name == b"onActivated" {
Some(VueApiKind::OnActivated)
} else {
None
}
}
13 => {
if name == b"onBeforeMount" {
Some(VueApiKind::OnBeforeMount)
} else if name == b"onDeactivated" {
Some(VueApiKind::OnDeactivated)
} else {
None
}
}
14 => {
if name == b"onBeforeUpdate" {
Some(VueApiKind::OnBeforeUpdate)
} else {
None
}
}
15 => {
if name == b"onBeforeUnmount" {
Some(VueApiKind::OnBeforeUnmount)
} else if name == b"onErrorCaptured" {
Some(VueApiKind::OnErrorCaptured)
} else if name == b"onRenderTracked" {
Some(VueApiKind::OnRenderTracked)
} else {
None
}
}
16 if name == b"onServerPrefetch" => Some(VueApiKind::OnServerPrefetch),
17 if name == b"onRenderTriggered" => Some(VueApiKind::OnRenderTriggered),
_ => None,
}
}
fn detect_u_api(name: &[u8], len: usize) -> Option<VueApiKind> {
match len {
8 => {
if name == b"useSlots" {
Some(VueApiKind::UseSlots)
} else if name == b"useAttrs" {
Some(VueApiKind::UseAttrs)
} else {
None
}
}
14 if name == b"useTemplateRef" => Some(VueApiKind::UseTemplateRef),
_ => None,
}
}
fn detect_g_api(name: &[u8], len: usize) -> Option<VueApiKind> {
if len == 18 && name == b"getCurrentInstance" {
Some(VueApiKind::GetCurrentInstance)
} else {
None
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProvideKeyKind {
StringLiteral,
Symbol,
Dynamic,
}
#[derive(Debug, Clone)]
pub struct ProvideKey {
pub span: Span,
pub kind: ProvideKeyKind,
}
#[derive(Debug, Clone)]
pub struct ProvideUsage {
pub span: Span,
pub key: ProvideKey,
pub value_span: Span,
}
#[derive(Debug, Clone)]
pub struct InjectUsage {
pub span: Span,
pub key: ProvideKey,
pub has_default: bool,
pub binding_span: Option<Span>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u8)]
pub enum LifecycleHook {
OnMounted,
OnUnmounted,
OnBeforeMount,
OnBeforeUnmount,
OnUpdated,
OnBeforeUpdate,
OnErrorCaptured,
OnActivated,
OnDeactivated,
OnRenderTracked,
OnRenderTriggered,
OnServerPrefetch,
}
impl LifecycleHook {
pub const fn from_api_kind(kind: VueApiKind) -> Option<Self> {
match kind {
VueApiKind::OnMounted => Some(Self::OnMounted),
VueApiKind::OnUnmounted => Some(Self::OnUnmounted),
VueApiKind::OnBeforeMount => Some(Self::OnBeforeMount),
VueApiKind::OnBeforeUnmount => Some(Self::OnBeforeUnmount),
VueApiKind::OnUpdated => Some(Self::OnUpdated),
VueApiKind::OnBeforeUpdate => Some(Self::OnBeforeUpdate),
VueApiKind::OnErrorCaptured => Some(Self::OnErrorCaptured),
VueApiKind::OnActivated => Some(Self::OnActivated),
VueApiKind::OnDeactivated => Some(Self::OnDeactivated),
VueApiKind::OnRenderTracked => Some(Self::OnRenderTracked),
VueApiKind::OnRenderTriggered => Some(Self::OnRenderTriggered),
VueApiKind::OnServerPrefetch => Some(Self::OnServerPrefetch),
_ => None,
}
}
}
#[derive(Debug, Clone)]
pub struct LifecycleUsage {
pub span: Span,
pub hook: LifecycleHook,
pub callback_span: Span,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReactiveKind {
Ref,
ShallowRef,
Reactive,
ShallowReactive,
Computed,
}
impl ReactiveKind {
pub const fn from_api_kind(kind: VueApiKind) -> Option<Self> {
match kind {
VueApiKind::Ref => Some(Self::Ref),
VueApiKind::ShallowRef => Some(Self::ShallowRef),
VueApiKind::Reactive => Some(Self::Reactive),
VueApiKind::ShallowReactive => Some(Self::ShallowReactive),
VueApiKind::Computed => Some(Self::Computed),
_ => None,
}
}
}
#[derive(Debug, Clone)]
pub struct ReactiveStateUsage {
pub kind: ReactiveKind,
pub binding_span: Span,
pub initializer_span: Option<Span>,
}
#[derive(Debug, Clone)]
pub struct WatcherUsage {
pub span: Span,
pub kind: VueApiKind,
pub callback_span: Span,
pub source_spans: Vec<Span>,
}
#[derive(Debug, Clone)]
pub enum EmitEventName {
Static { span: Span },
Dynamic { span: Span },
}
#[derive(Debug, Clone)]
pub struct EmitCallUsage {
pub span: Span,
pub event_name: EmitEventName,
pub arg_spans: Vec<Span>,
}
#[derive(Debug, Clone)]
pub struct TemplateUtilUsage {
pub span: Span,
pub kind: VueApiKind,
pub binding_span: Option<Span>,
pub ref_name_span: Option<Span>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CallSiteContext {
BeforeAwait,
AfterAwait,
InLifecycleCallback,
InReactiveCallback,
InTimerCallback,
InPromiseCallback,
Unknown,
}
impl CallSiteContext {
pub const fn is_safe(&self) -> bool {
matches!(
self,
Self::BeforeAwait | Self::InLifecycleCallback | Self::InReactiveCallback
)
}
pub const fn is_unsafe(&self) -> bool {
matches!(self, Self::InTimerCallback)
}
pub const fn is_potentially_unsafe(&self) -> bool {
matches!(self, Self::AfterAwait | Self::InPromiseCallback)
}
pub const fn description(&self) -> &'static str {
match self {
Self::BeforeAwait => "before any await (safe)",
Self::AfterAwait => "after await (potentially unsafe)",
Self::InLifecycleCallback => "inside lifecycle hook (safe)",
Self::InReactiveCallback => "inside computed/watch (safe)",
Self::InTimerCallback => "inside setTimeout/setInterval (unsafe)",
Self::InPromiseCallback => "inside Promise callback (potentially unsafe)",
Self::Unknown => "unknown context",
}
}
}
#[derive(Debug, Clone)]
pub struct SyncContextUsage {
pub span: Span,
pub kind: VueApiKind,
pub context: CallSiteContext,
pub binding_span: Option<Span>,
pub preceding_await_span: Option<Span>,
}
impl SyncContextUsage {
pub fn is_safe(&self) -> bool {
self.context.is_safe()
}
pub fn is_potentially_unsafe(&self) -> bool {
self.context.is_potentially_unsafe() || self.context.is_unsafe()
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct FileUsageFlags {
bits: u32,
}
impl FileUsageFlags {
pub const HAS_PROVIDE: u32 = 1 << 0;
pub const HAS_INJECT: u32 = 1 << 1;
pub const HAS_LIFECYCLE_HOOKS: u32 = 1 << 2;
pub const HAS_REACTIVE_STATE: u32 = 1 << 3;
pub const HAS_WATCHERS: u32 = 1 << 4;
pub const HAS_EMIT_CALLS: u32 = 1 << 5;
pub const HAS_TEMPLATE_UTILS: u32 = 1 << 6;
pub const IS_ASYNC_SETUP: u32 = 1 << 7;
pub const HAS_TEMPLATE_REFS: u32 = 1 << 8;
pub const HAS_SLOT_USAGE: u32 = 1 << 9;
pub const HAS_COMPONENT_USAGE: u32 = 1 << 10;
pub const HAS_SLOT_DEFINITIONS: u32 = 1 << 11;
pub const HAS_SYNC_CONTEXT_USAGE: u32 = 1 << 12;
pub const HAS_UNSAFE_SYNC_CONTEXT: u32 = 1 << 13;
#[inline]
pub fn set(&mut self, flag: u32) {
self.bits |= flag;
}
#[inline]
pub const fn has(&self, flag: u32) -> bool {
(self.bits & flag) != 0
}
#[inline]
pub const fn bits(&self) -> u32 {
self.bits
}
}
#[derive(Debug)]
pub struct UsageCollector<'a> {
pub provides: Vec<ProvideUsage>,
pub injects: Vec<InjectUsage>,
pub lifecycle: Vec<LifecycleUsage>,
pub reactive: Vec<ReactiveStateUsage>,
pub watchers: Vec<WatcherUsage>,
pub emit_calls: Vec<EmitCallUsage>,
pub template_utils: Vec<TemplateUtilUsage>,
pub sync_context_usages: Vec<SyncContextUsage>,
pub flags: FileUsageFlags,
_source: &'a [u8],
first_await_span: Option<Span>,
}
impl<'a> UsageCollector<'a> {
pub fn new(source: &'a [u8]) -> Self {
Self {
provides: Vec::with_capacity(2),
injects: Vec::with_capacity(4),
lifecycle: Vec::with_capacity(4),
reactive: Vec::with_capacity(8),
watchers: Vec::with_capacity(4),
emit_calls: Vec::with_capacity(4),
template_utils: Vec::with_capacity(2),
sync_context_usages: Vec::with_capacity(2),
flags: FileUsageFlags::default(),
_source: source,
first_await_span: None,
}
}
pub fn with_size_hint(source: &'a [u8]) -> Self {
let len = source.len();
let (reactive_cap, lifecycle_cap, watcher_cap) = if len < 1000 {
(4, 2, 2)
} else if len < 5000 {
(8, 4, 4)
} else {
(16, 8, 8)
};
Self {
provides: Vec::with_capacity(2),
injects: Vec::with_capacity(4),
lifecycle: Vec::with_capacity(lifecycle_cap),
reactive: Vec::with_capacity(reactive_cap),
watchers: Vec::with_capacity(watcher_cap),
emit_calls: Vec::with_capacity(4),
template_utils: Vec::with_capacity(2),
sync_context_usages: Vec::with_capacity(2),
flags: FileUsageFlags::default(),
_source: source,
first_await_span: None,
}
}
#[inline]
pub fn record_await(&mut self, span: Span) {
if self.first_await_span.is_none() {
self.first_await_span = Some(span);
}
}
#[inline]
pub fn has_await_before(&self, pos: u32) -> bool {
self.first_await_span.is_some_and(|s| s.end < pos)
}
#[inline]
pub fn first_await_span(&self) -> Option<Span> {
self.first_await_span
}
#[inline]
pub fn record_provide(&mut self, usage: ProvideUsage) {
self.flags.set(FileUsageFlags::HAS_PROVIDE);
self.provides.push(usage);
}
#[inline]
pub fn record_inject(&mut self, usage: InjectUsage) {
self.flags.set(FileUsageFlags::HAS_INJECT);
self.injects.push(usage);
}
#[inline]
pub fn record_lifecycle(&mut self, usage: LifecycleUsage) {
self.flags.set(FileUsageFlags::HAS_LIFECYCLE_HOOKS);
self.lifecycle.push(usage);
}
#[inline]
pub fn record_reactive(&mut self, usage: ReactiveStateUsage) {
self.flags.set(FileUsageFlags::HAS_REACTIVE_STATE);
self.reactive.push(usage);
}
#[inline]
pub fn record_watcher(&mut self, usage: WatcherUsage) {
self.flags.set(FileUsageFlags::HAS_WATCHERS);
self.watchers.push(usage);
}
#[inline]
pub fn record_emit(&mut self, usage: EmitCallUsage) {
self.flags.set(FileUsageFlags::HAS_EMIT_CALLS);
self.emit_calls.push(usage);
}
#[inline]
pub fn record_template_util(&mut self, usage: TemplateUtilUsage) {
self.flags.set(FileUsageFlags::HAS_TEMPLATE_UTILS);
self.template_utils.push(usage);
}
#[inline]
pub fn record_sync_context_usage(&mut self, usage: SyncContextUsage) {
self.flags.set(FileUsageFlags::HAS_SYNC_CONTEXT_USAGE);
if usage.is_potentially_unsafe() {
self.flags.set(FileUsageFlags::HAS_UNSAFE_SYNC_CONTEXT);
}
self.sync_context_usages.push(usage);
}
pub fn is_empty(&self) -> bool {
self.flags.bits() == 0
}
pub fn has_unsafe_sync_context(&self) -> bool {
self.flags.has(FileUsageFlags::HAS_UNSAFE_SYNC_CONTEXT)
}
pub fn unsafe_sync_context_usages(&self) -> impl Iterator<Item = &SyncContextUsage> {
self.sync_context_usages
.iter()
.filter(|u| u.is_potentially_unsafe())
}
}
#[derive(Debug, Clone)]
pub struct TemplateRefAttrUsage {
pub name_span: Span,
pub element_id: u32,
pub is_dynamic: bool,
}
#[derive(Debug, Clone)]
pub enum SlotName {
Default,
Named { span: Span },
Dynamic { span: Span },
}
#[derive(Debug, Clone)]
pub struct SlotUsageInfo {
pub span: Span,
pub name: SlotName,
pub element_id: u32,
pub scope_binding_spans: Vec<Span>,
}
#[derive(Debug, Clone)]
pub struct SlotDefinitionInfo {
pub span: Span,
pub name_span: Option<Span>,
pub element_id: u32,
}
#[derive(Debug, Clone)]
pub struct ComponentUsageInfo {
pub span: Span,
pub name_span: Span,
pub element_id: u32,
pub is_dynamic: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BindingRefContext {
Interpolation,
DirectiveValue,
DirectiveArg,
EventHandler,
}
#[derive(Debug, Clone)]
pub struct BindingRefInfo {
pub name_span: Span,
pub element_id: u32,
pub scope_id: u32,
pub context: BindingRefContext,
}
#[derive(Debug, Default)]
pub struct TemplateUsageCollector {
pub ref_attrs: Vec<TemplateRefAttrUsage>,
pub slot_usages: Vec<SlotUsageInfo>,
pub slot_definitions: Vec<SlotDefinitionInfo>,
pub component_usages: Vec<ComponentUsageInfo>,
pub binding_refs: Vec<BindingRefInfo>,
pub loops: Vec<LoopInfo>,
pub render_warnings: Vec<RenderPatternWarning>,
pub metrics: TemplateMetrics,
pub flags: FileUsageFlags,
}
impl TemplateUsageCollector {
pub fn new() -> Self {
Self {
ref_attrs: Vec::with_capacity(4),
slot_usages: Vec::with_capacity(4),
slot_definitions: Vec::with_capacity(2),
component_usages: Vec::with_capacity(8),
binding_refs: Vec::with_capacity(16),
loops: Vec::with_capacity(4),
render_warnings: Vec::new(),
metrics: TemplateMetrics::default(),
flags: FileUsageFlags::default(),
}
}
#[inline]
pub fn record_ref_attr(&mut self, usage: TemplateRefAttrUsage) {
self.flags.set(FileUsageFlags::HAS_TEMPLATE_REFS);
self.ref_attrs.push(usage);
}
#[inline]
pub fn record_slot_usage(&mut self, usage: SlotUsageInfo) {
self.flags.set(FileUsageFlags::HAS_SLOT_USAGE);
self.slot_usages.push(usage);
}
#[inline]
pub fn record_slot_definition(&mut self, info: SlotDefinitionInfo) {
self.flags.set(FileUsageFlags::HAS_SLOT_DEFINITIONS);
self.slot_definitions.push(info);
}
#[inline]
pub fn record_component_usage(&mut self, usage: ComponentUsageInfo) {
self.flags.set(FileUsageFlags::HAS_COMPONENT_USAGE);
self.component_usages.push(usage);
}
#[inline]
pub fn record_binding_ref(&mut self, info: BindingRefInfo) {
self.binding_refs.push(info);
}
#[inline]
pub fn record_loop(&mut self, info: LoopInfo) {
self.metrics.loop_count += 1;
if info.depth > self.metrics.max_loop_depth {
self.metrics.max_loop_depth = info.depth;
}
let skip_warnings = info.in_unreachable_branch || info.iterable_type.is_static();
if !skip_warnings {
if !info.has_key {
self.render_warnings
.push(RenderPatternWarning::LoopWithoutKey {
loop_span: info.span,
element_id: info.element_id,
});
}
if info.has_condition_on_same {
self.render_warnings
.push(RenderPatternWarning::LoopWithConditionOnSame {
loop_span: info.span,
element_id: info.element_id,
});
}
if info.depth >= 2 {
if let Some(parent_id) = info.parent_loop_id {
if let Some(parent_loop) = self.loops.iter().find(|l| l.element_id == parent_id)
{
if !parent_loop.in_unreachable_branch {
self.render_warnings.push(RenderPatternWarning::NestedLoop {
outer_span: parent_loop.span,
inner_span: info.span,
depth: info.depth,
});
}
}
}
}
}
self.loops.push(info);
}
#[inline]
pub fn record_warning(&mut self, warning: RenderPatternWarning) {
self.render_warnings.push(warning);
}
#[inline]
pub fn increment_element_count(&mut self) {
self.metrics.element_count += 1;
}
#[inline]
pub fn increment_interpolation_count(&mut self) {
self.metrics.interpolation_count += 1;
}
#[inline]
pub fn increment_directive_count(&mut self) {
self.metrics.directive_count += 1;
}
#[inline]
pub fn record_conditional(&mut self, chain_length: u32) {
self.metrics.conditional_count += 1;
if chain_length > self.metrics.max_conditional_chain {
self.metrics.max_conditional_chain = chain_length;
}
}
pub fn finalize_metrics(&mut self) {
self.metrics.component_count = self.component_usages.len() as u32;
self.metrics.slot_definition_count = self.slot_definitions.len() as u32;
self.metrics.slot_usage_count = self.slot_usages.len() as u32;
self.metrics.ref_count = self.ref_attrs.len() as u32;
let mut unique_spans: std::collections::HashSet<(u32, u32)> =
std::collections::HashSet::new();
for binding_ref in &self.binding_refs {
unique_spans.insert((binding_ref.name_span.start, binding_ref.name_span.end));
}
self.metrics.unique_binding_refs = unique_spans.len() as u32;
}
pub fn is_empty(&self) -> bool {
self.flags.bits() == 0 && self.binding_refs.is_empty()
}
pub fn warnings_by_severity(&self, severity: WarningSeverity) -> Vec<&RenderPatternWarning> {
self.render_warnings
.iter()
.filter(|w| w.severity() == severity)
.collect()
}
pub fn has_render_warnings(&self) -> bool {
!self.render_warnings.is_empty()
}
pub fn take(&mut self) -> Self {
std::mem::take(self)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum StaticConditionValue {
AlwaysTrue,
AlwaysFalse,
#[default]
Dynamic,
}
impl StaticConditionValue {
pub fn from_expression_bytes(expr: &[u8]) -> Self {
let trimmed = expr.trim_ascii();
if trimmed == b"true" {
return Self::AlwaysTrue;
}
if trimmed == b"false" {
return Self::AlwaysFalse;
}
if trimmed == b"null" || trimmed == b"undefined" {
return Self::AlwaysFalse;
}
if let Ok(s) = std::str::from_utf8(trimmed) {
if let Ok(n) = s.parse::<f64>() {
return if n == 0.0 {
Self::AlwaysFalse
} else {
Self::AlwaysTrue
};
}
}
if trimmed == b"''" || trimmed == b"\"\"" {
return Self::AlwaysFalse;
}
if trimmed.len() >= 2 {
let first = trimmed[0];
let last = trimmed[trimmed.len() - 1];
if (first == b'\'' && last == b'\'') || (first == b'"' && last == b'"') {
return Self::AlwaysTrue;
}
}
Self::Dynamic
}
pub const fn is_unreachable(&self) -> bool {
matches!(self, Self::AlwaysFalse)
}
pub const fn siblings_unreachable(&self) -> bool {
matches!(self, Self::AlwaysTrue)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum IterableType {
StaticNumber(u32),
#[default]
Dynamic,
}
impl IterableType {
pub fn from_expression_bytes(expr: &[u8]) -> Self {
let trimmed = expr.trim_ascii();
if let Ok(s) = std::str::from_utf8(trimmed) {
if let Ok(n) = s.parse::<u32>() {
return Self::StaticNumber(n);
}
}
Self::Dynamic
}
pub const fn is_static(&self) -> bool {
matches!(self, Self::StaticNumber(_))
}
pub const fn static_count(&self) -> Option<u32> {
match self {
Self::StaticNumber(n) => Some(*n),
Self::Dynamic => None,
}
}
}
#[derive(Debug, Clone)]
pub struct LoopInfo {
pub span: Span,
pub element_id: u32,
pub depth: u32,
pub parent_loop_id: Option<u32>,
pub has_key: bool,
pub has_condition_on_same: bool,
pub iterable_span: Span,
pub iterable_type: IterableType,
pub in_unreachable_branch: bool,
}
#[derive(Debug, Clone, Default)]
pub struct LoopChildren {
pub component_count: u32,
pub element_count: u32,
pub external_binding_refs: Vec<Span>,
pub loop_var_refs: Vec<Span>,
}
#[derive(Debug, Clone)]
pub enum RenderPatternWarning {
LoopWithoutKey { loop_span: Span, element_id: u32 },
LoopWithConditionOnSame { loop_span: Span, element_id: u32 },
NestedLoop {
outer_span: Span,
inner_span: Span,
depth: u32,
},
LoopWithExternalDeps {
loop_span: Span,
element_id: u32,
external_dep_spans: Vec<Span>,
},
LoopWithoutComponents {
loop_span: Span,
element_id: u32,
element_count: u32,
},
}
impl RenderPatternWarning {
pub fn span(&self) -> Span {
match self {
Self::LoopWithoutKey { loop_span, .. } => *loop_span,
Self::LoopWithConditionOnSame { loop_span, .. } => *loop_span,
Self::NestedLoop { inner_span, .. } => *inner_span,
Self::LoopWithExternalDeps { loop_span, .. } => *loop_span,
Self::LoopWithoutComponents { loop_span, .. } => *loop_span,
}
}
pub const fn message(&self) -> &'static str {
match self {
Self::LoopWithoutKey { .. } => {
"v-for without :key - may cause rendering issues and performance degradation"
}
Self::LoopWithConditionOnSame { .. } => {
"v-if on same element as v-for - use <template v-for> or computed filtering"
}
Self::NestedLoop { .. } => {
"Nested loops create O(n×m) render complexity - consider pagination or virtualization"
}
Self::LoopWithExternalDeps { .. } => {
"Loop body depends on external bindings - extract to child component for caching"
}
Self::LoopWithoutComponents { .. } => {
"Loop renders raw elements - consider component extraction for complex items"
}
}
}
pub const fn severity(&self) -> WarningSeverity {
match self {
Self::LoopWithoutKey { .. } => WarningSeverity::Warning,
Self::LoopWithConditionOnSame { .. } => WarningSeverity::Warning,
Self::NestedLoop { depth, .. } => {
if *depth >= 3 {
WarningSeverity::Warning
} else {
WarningSeverity::Info
}
}
Self::LoopWithExternalDeps { .. } => WarningSeverity::Info,
Self::LoopWithoutComponents { .. } => WarningSeverity::Hint,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WarningSeverity {
Hint,
Info,
Warning,
}
#[derive(Debug, Clone, Default)]
pub struct TemplateMetrics {
pub element_count: u32,
pub component_count: u32,
pub loop_count: u32,
pub max_loop_depth: u32,
pub conditional_count: u32,
pub max_conditional_chain: u32,
pub interpolation_count: u32,
pub directive_count: u32,
pub slot_definition_count: u32,
pub slot_usage_count: u32,
pub ref_count: u32,
pub unique_binding_refs: u32,
}
impl TemplateMetrics {
pub fn complexity_score(&self) -> f32 {
let base = self.element_count as f32 * 0.5
+ self.component_count as f32 * 1.0
+ self.interpolation_count as f32 * 0.3
+ self.directive_count as f32 * 0.5;
let loop_penalty =
self.loop_count as f32 * 2.0 + (self.max_loop_depth.saturating_sub(1)) as f32 * 5.0;
let conditional_penalty = self.conditional_count as f32 * 0.5
+ (self.max_conditional_chain.saturating_sub(2)) as f32 * 1.0;
let slot_complexity =
self.slot_definition_count as f32 * 1.5 + self.slot_usage_count as f32 * 1.0;
base + loop_penalty + conditional_penalty + slot_complexity
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConditionLikelihood {
LikelyTrue,
LikelyFalse,
Unknown,
}
impl ConditionLikelihood {
pub fn from_binding_name(name: &str) -> Self {
let lower = name.to_lowercase();
const USUALLY_FALSE: &[&str] = &[
"loading",
"isloading",
"pending",
"ispending",
"fetching",
"isfetching",
"error",
"haserror",
"iserror",
"failed",
"hasfailed",
"invalid",
"isinvalid",
"admin",
"isadmin",
"moderator",
"ismoderator",
"superuser",
"issuperuser",
"debug",
"isdebug",
"dev",
"isdev",
"empty",
"isempty",
"disabled",
"isdisabled",
];
const USUALLY_TRUE: &[&str] = &[
"visible",
"isvisible",
"active",
"isactive",
"enabled",
"isenabled",
"ready",
"isready",
"loaded",
"isloaded",
"valid",
"isvalid",
"authenticated",
"isauthenticated",
"loggedin",
"isloggedin",
];
for pattern in USUALLY_FALSE {
if lower.contains(pattern) {
return Self::LikelyFalse;
}
}
for pattern in USUALLY_TRUE {
if lower.contains(pattern) {
return Self::LikelyTrue;
}
}
Self::Unknown
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_detect_provide_inject() {
assert_eq!(detect_vue_api_call(b"provide"), Some(VueApiKind::Provide));
assert_eq!(detect_vue_api_call(b"inject"), Some(VueApiKind::Inject));
}
#[test]
fn test_detect_reactivity() {
assert_eq!(detect_vue_api_call(b"ref"), Some(VueApiKind::Ref));
assert_eq!(detect_vue_api_call(b"reactive"), Some(VueApiKind::Reactive));
assert_eq!(detect_vue_api_call(b"computed"), Some(VueApiKind::Computed));
assert_eq!(
detect_vue_api_call(b"shallowRef"),
Some(VueApiKind::ShallowRef)
);
assert_eq!(
detect_vue_api_call(b"shallowReactive"),
Some(VueApiKind::ShallowReactive)
);
}
#[test]
fn test_detect_watchers() {
assert_eq!(detect_vue_api_call(b"watch"), Some(VueApiKind::Watch));
assert_eq!(
detect_vue_api_call(b"watchEffect"),
Some(VueApiKind::WatchEffect)
);
assert_eq!(
detect_vue_api_call(b"watchPostEffect"),
Some(VueApiKind::WatchPostEffect)
);
assert_eq!(
detect_vue_api_call(b"watchSyncEffect"),
Some(VueApiKind::WatchSyncEffect)
);
}
#[test]
fn test_detect_lifecycle_hooks() {
assert_eq!(
detect_vue_api_call(b"onMounted"),
Some(VueApiKind::OnMounted)
);
assert_eq!(
detect_vue_api_call(b"onUnmounted"),
Some(VueApiKind::OnUnmounted)
);
assert_eq!(
detect_vue_api_call(b"onBeforeMount"),
Some(VueApiKind::OnBeforeMount)
);
assert_eq!(
detect_vue_api_call(b"onBeforeUnmount"),
Some(VueApiKind::OnBeforeUnmount)
);
assert_eq!(
detect_vue_api_call(b"onUpdated"),
Some(VueApiKind::OnUpdated)
);
assert_eq!(
detect_vue_api_call(b"onBeforeUpdate"),
Some(VueApiKind::OnBeforeUpdate)
);
assert_eq!(
detect_vue_api_call(b"onErrorCaptured"),
Some(VueApiKind::OnErrorCaptured)
);
assert_eq!(
detect_vue_api_call(b"onActivated"),
Some(VueApiKind::OnActivated)
);
assert_eq!(
detect_vue_api_call(b"onDeactivated"),
Some(VueApiKind::OnDeactivated)
);
assert_eq!(
detect_vue_api_call(b"onRenderTracked"),
Some(VueApiKind::OnRenderTracked)
);
assert_eq!(
detect_vue_api_call(b"onRenderTriggered"),
Some(VueApiKind::OnRenderTriggered)
);
assert_eq!(
detect_vue_api_call(b"onServerPrefetch"),
Some(VueApiKind::OnServerPrefetch)
);
}
#[test]
fn test_detect_template_utils() {
assert_eq!(detect_vue_api_call(b"useSlots"), Some(VueApiKind::UseSlots));
assert_eq!(detect_vue_api_call(b"useAttrs"), Some(VueApiKind::UseAttrs));
assert_eq!(
detect_vue_api_call(b"useTemplateRef"),
Some(VueApiKind::UseTemplateRef)
);
}
#[test]
fn test_non_vue_apis() {
assert_eq!(detect_vue_api_call(b"re"), None);
assert_eq!(detect_vue_api_call(b"onRenderTriggeredExtra"), None);
assert_eq!(detect_vue_api_call(b"console"), None);
assert_eq!(detect_vue_api_call(b"defineProps"), None); assert_eq!(detect_vue_api_call(b"useState"), None); }
#[test]
fn test_api_categories() {
assert_eq!(
VueApiKind::Provide.category(),
VueApiCategory::DependencyInjection
);
assert_eq!(VueApiKind::Ref.category(), VueApiCategory::Reactivity);
assert_eq!(VueApiKind::Watch.category(), VueApiCategory::Watchers);
assert_eq!(VueApiKind::OnMounted.category(), VueApiCategory::Lifecycle);
assert_eq!(
VueApiKind::UseSlots.category(),
VueApiCategory::TemplateUtils
);
assert_eq!(
VueApiKind::GetCurrentInstance.category(),
VueApiCategory::InstanceAccess
);
}
#[test]
fn test_file_usage_flags() {
let mut flags = FileUsageFlags::default();
assert!(!flags.has(FileUsageFlags::HAS_PROVIDE));
flags.set(FileUsageFlags::HAS_PROVIDE);
assert!(flags.has(FileUsageFlags::HAS_PROVIDE));
assert!(!flags.has(FileUsageFlags::HAS_INJECT));
flags.set(FileUsageFlags::HAS_INJECT);
assert!(flags.has(FileUsageFlags::HAS_PROVIDE));
assert!(flags.has(FileUsageFlags::HAS_INJECT));
}
#[test]
fn test_detect_get_current_instance() {
assert_eq!(
detect_vue_api_call(b"getCurrentInstance"),
Some(VueApiKind::GetCurrentInstance)
);
assert_eq!(detect_vue_api_call(b"getcurrentinstance"), None);
assert_eq!(detect_vue_api_call(b"GetCurrentInstance"), None);
}
#[test]
fn test_get_current_instance_category() {
assert_eq!(
VueApiKind::GetCurrentInstance.category(),
VueApiCategory::InstanceAccess
);
}
#[test]
fn test_get_current_instance_requires_sync_context() {
assert!(VueApiKind::GetCurrentInstance.requires_sync_context());
assert!(!VueApiKind::Ref.requires_sync_context());
assert!(!VueApiKind::OnMounted.requires_sync_context());
}
#[test]
fn test_call_site_context_safety() {
assert!(CallSiteContext::BeforeAwait.is_safe());
assert!(CallSiteContext::InLifecycleCallback.is_safe());
assert!(CallSiteContext::InReactiveCallback.is_safe());
assert!(CallSiteContext::AfterAwait.is_potentially_unsafe());
assert!(CallSiteContext::InPromiseCallback.is_potentially_unsafe());
assert!(CallSiteContext::InTimerCallback.is_unsafe());
assert!(!CallSiteContext::Unknown.is_safe());
assert!(!CallSiteContext::Unknown.is_unsafe());
}
#[test]
fn test_sync_context_usage() {
let safe_usage = SyncContextUsage {
span: Span::new(0, 20),
kind: VueApiKind::GetCurrentInstance,
context: CallSiteContext::BeforeAwait,
binding_span: Some(Span::new(6, 14)),
preceding_await_span: None,
};
assert!(safe_usage.is_safe());
assert!(!safe_usage.is_potentially_unsafe());
let unsafe_usage = SyncContextUsage {
span: Span::new(50, 70),
kind: VueApiKind::GetCurrentInstance,
context: CallSiteContext::AfterAwait,
binding_span: Some(Span::new(56, 64)),
preceding_await_span: Some(Span::new(0, 25)),
};
assert!(!unsafe_usage.is_safe());
assert!(unsafe_usage.is_potentially_unsafe());
}
#[test]
fn test_usage_collector_await_tracking() {
let source = b"const x = 1;";
let mut collector = UsageCollector::new(source);
assert!(!collector.has_await_before(0));
assert!(!collector.has_await_before(100));
assert!(collector.first_await_span().is_none());
collector.record_await(Span::new(10, 25));
assert!(!collector.has_await_before(0)); assert!(!collector.has_await_before(10)); assert!(!collector.has_await_before(25)); assert!(collector.has_await_before(26)); assert!(collector.has_await_before(100));
assert_eq!(collector.first_await_span(), Some(Span::new(10, 25)));
collector.record_await(Span::new(50, 60));
assert_eq!(collector.first_await_span(), Some(Span::new(10, 25)));
}
#[test]
fn test_usage_collector_sync_context_recording() {
let source = b"const instance = getCurrentInstance();";
let mut collector = UsageCollector::new(source);
let safe = SyncContextUsage {
span: Span::new(17, 37),
kind: VueApiKind::GetCurrentInstance,
context: CallSiteContext::BeforeAwait,
binding_span: Some(Span::new(6, 14)),
preceding_await_span: None,
};
collector.record_sync_context_usage(safe);
assert!(collector.flags.has(FileUsageFlags::HAS_SYNC_CONTEXT_USAGE));
assert!(!collector.flags.has(FileUsageFlags::HAS_UNSAFE_SYNC_CONTEXT));
assert!(!collector.has_unsafe_sync_context());
assert_eq!(collector.sync_context_usages.len(), 1);
let unsafe_usage = SyncContextUsage {
span: Span::new(100, 120),
kind: VueApiKind::GetCurrentInstance,
context: CallSiteContext::AfterAwait,
binding_span: None,
preceding_await_span: Some(Span::new(50, 60)),
};
collector.record_sync_context_usage(unsafe_usage);
assert!(collector.flags.has(FileUsageFlags::HAS_UNSAFE_SYNC_CONTEXT));
assert!(collector.has_unsafe_sync_context());
assert_eq!(collector.sync_context_usages.len(), 2);
let unsafe_count = collector.unsafe_sync_context_usages().count();
assert_eq!(unsafe_count, 1);
}
#[test]
fn test_loop_info_recording() {
let mut collector = TemplateUsageCollector::new();
collector.record_loop(LoopInfo {
span: Span::new(10, 50),
element_id: 1,
depth: 1,
parent_loop_id: None,
has_key: false,
has_condition_on_same: false,
iterable_span: Span::new(30, 35),
iterable_type: IterableType::Dynamic,
in_unreachable_branch: false,
});
assert_eq!(collector.loops.len(), 1);
assert_eq!(collector.metrics.loop_count, 1);
assert_eq!(collector.metrics.max_loop_depth, 1);
assert!(collector.has_render_warnings());
assert_eq!(collector.render_warnings.len(), 1);
assert!(matches!(
collector.render_warnings[0],
RenderPatternWarning::LoopWithoutKey { .. }
));
}
#[test]
fn test_loop_with_key_no_warning() {
let mut collector = TemplateUsageCollector::new();
collector.record_loop(LoopInfo {
span: Span::new(10, 50),
element_id: 1,
depth: 1,
parent_loop_id: None,
has_key: true, has_condition_on_same: false,
iterable_span: Span::new(30, 35),
iterable_type: IterableType::Dynamic,
in_unreachable_branch: false,
});
assert!(!collector.has_render_warnings());
}
#[test]
fn test_loop_with_condition_warning() {
let mut collector = TemplateUsageCollector::new();
collector.record_loop(LoopInfo {
span: Span::new(10, 50),
element_id: 1,
depth: 1,
parent_loop_id: None,
has_key: true,
has_condition_on_same: true, iterable_span: Span::new(30, 35),
iterable_type: IterableType::Dynamic,
in_unreachable_branch: false,
});
assert!(collector.has_render_warnings());
assert!(matches!(
collector.render_warnings[0],
RenderPatternWarning::LoopWithConditionOnSame { .. }
));
}
#[test]
fn test_nested_loop_tracking() {
let mut collector = TemplateUsageCollector::new();
collector.record_loop(LoopInfo {
span: Span::new(10, 100),
element_id: 1,
depth: 1,
parent_loop_id: None,
has_key: true,
has_condition_on_same: false,
iterable_span: Span::new(30, 35),
iterable_type: IterableType::Dynamic,
in_unreachable_branch: false,
});
collector.record_loop(LoopInfo {
span: Span::new(40, 80),
element_id: 2,
depth: 2,
parent_loop_id: Some(1),
has_key: true,
has_condition_on_same: false,
iterable_span: Span::new(50, 55),
iterable_type: IterableType::Dynamic,
in_unreachable_branch: false,
});
assert_eq!(collector.loops.len(), 2);
assert_eq!(collector.metrics.loop_count, 2);
assert_eq!(collector.metrics.max_loop_depth, 2);
let nested_warnings: Vec<_> = collector
.render_warnings
.iter()
.filter(|w| matches!(w, RenderPatternWarning::NestedLoop { .. }))
.collect();
assert_eq!(nested_warnings.len(), 1);
}
#[test]
fn test_template_metrics_complexity_score() {
let metrics = TemplateMetrics {
element_count: 10,
component_count: 3,
loop_count: 2,
max_loop_depth: 2, conditional_count: 4,
max_conditional_chain: 3,
interpolation_count: 5,
directive_count: 8,
slot_definition_count: 1,
slot_usage_count: 2,
ref_count: 2,
unique_binding_refs: 6,
};
let score = metrics.complexity_score();
assert!(score > 0.0);
let simple_metrics = TemplateMetrics {
element_count: 10,
component_count: 3,
loop_count: 1,
max_loop_depth: 1, ..Default::default()
};
assert!(metrics.complexity_score() > simple_metrics.complexity_score());
}
#[test]
fn test_render_pattern_warning_severity() {
let no_key = RenderPatternWarning::LoopWithoutKey {
loop_span: Span::new(0, 10),
element_id: 1,
};
assert_eq!(no_key.severity(), WarningSeverity::Warning);
let nested = RenderPatternWarning::NestedLoop {
outer_span: Span::new(0, 100),
inner_span: Span::new(20, 80),
depth: 2,
};
assert_eq!(nested.severity(), WarningSeverity::Info);
let deep_nested = RenderPatternWarning::NestedLoop {
outer_span: Span::new(0, 100),
inner_span: Span::new(20, 80),
depth: 3,
};
assert_eq!(deep_nested.severity(), WarningSeverity::Warning);
}
#[test]
fn test_condition_likelihood_usually_false() {
assert_eq!(
ConditionLikelihood::from_binding_name("isLoading"),
ConditionLikelihood::LikelyFalse
);
assert_eq!(
ConditionLikelihood::from_binding_name("hasError"),
ConditionLikelihood::LikelyFalse
);
assert_eq!(
ConditionLikelihood::from_binding_name("isAdmin"),
ConditionLikelihood::LikelyFalse
);
assert_eq!(
ConditionLikelihood::from_binding_name("isEmpty"),
ConditionLikelihood::LikelyFalse
);
}
#[test]
fn test_condition_likelihood_usually_true() {
assert_eq!(
ConditionLikelihood::from_binding_name("isVisible"),
ConditionLikelihood::LikelyTrue
);
assert_eq!(
ConditionLikelihood::from_binding_name("isActive"),
ConditionLikelihood::LikelyTrue
);
assert_eq!(
ConditionLikelihood::from_binding_name("isReady"),
ConditionLikelihood::LikelyTrue
);
assert_eq!(
ConditionLikelihood::from_binding_name("isAuthenticated"),
ConditionLikelihood::LikelyTrue
);
}
#[test]
fn test_condition_likelihood_unknown() {
assert_eq!(
ConditionLikelihood::from_binding_name("showModal"),
ConditionLikelihood::Unknown
);
assert_eq!(
ConditionLikelihood::from_binding_name("count"),
ConditionLikelihood::Unknown
);
assert_eq!(
ConditionLikelihood::from_binding_name("someFlag"),
ConditionLikelihood::Unknown
);
}
#[test]
fn test_template_usage_collector_finalize() {
let mut collector = TemplateUsageCollector::new();
collector.record_component_usage(ComponentUsageInfo {
span: Span::new(0, 10),
name_span: Span::new(1, 9),
element_id: 1,
is_dynamic: false,
});
collector.record_binding_ref(BindingRefInfo {
name_span: Span::new(10, 15),
element_id: 1,
scope_id: 0,
context: BindingRefContext::Interpolation,
});
collector.record_binding_ref(BindingRefInfo {
name_span: Span::new(10, 15), element_id: 2,
scope_id: 0,
context: BindingRefContext::DirectiveValue,
});
collector.finalize_metrics();
assert_eq!(collector.metrics.component_count, 1);
assert_eq!(collector.metrics.unique_binding_refs, 1); }
}