mod helpers;
mod template;
pub use helpers::{
extract_identifiers_oxc, extract_inline_callback_params, extract_slot_props,
is_builtin_directive, is_component_tag, is_keyword, parse_v_for_expression, strip_js_comments,
};
use crate::analysis::Croquis;
use vize_carton::{profile, CompactString};
#[derive(Debug, Clone, Copy, Default)]
pub struct AnalyzerOptions {
pub analyze_script: bool,
pub analyze_template_scopes: bool,
pub track_usage: bool,
pub detect_undefined: bool,
pub analyze_hoisting: bool,
pub collect_template_expressions: bool,
}
impl AnalyzerOptions {
#[inline]
pub const fn full() -> Self {
Self {
analyze_script: true,
analyze_template_scopes: true,
track_usage: true,
detect_undefined: true,
analyze_hoisting: true,
collect_template_expressions: true,
}
}
#[inline]
pub const fn for_lint() -> Self {
Self {
analyze_script: true,
analyze_template_scopes: true,
track_usage: true,
detect_undefined: true,
analyze_hoisting: false,
collect_template_expressions: false,
}
}
#[inline]
pub const fn for_compile() -> Self {
Self {
analyze_script: true,
analyze_template_scopes: true,
track_usage: true,
detect_undefined: false,
analyze_hoisting: true,
collect_template_expressions: false,
}
}
}
pub struct Analyzer {
pub(crate) options: AnalyzerOptions,
pub(crate) summary: Croquis,
pub(crate) script_analyzed: bool,
pub(crate) vif_guard_stack: Vec<CompactString>,
}
impl Analyzer {
#[inline]
pub fn new() -> Self {
Self::with_options(AnalyzerOptions::default())
}
#[inline]
pub fn with_options(options: AnalyzerOptions) -> Self {
Self {
options,
summary: Croquis::new(),
script_analyzed: false,
vif_guard_stack: Vec::new(),
}
}
pub(crate) fn current_vif_guard(&self) -> Option<CompactString> {
if self.vif_guard_stack.is_empty() {
None
} else {
Some(CompactString::new(self.vif_guard_stack.join(" && ")))
}
}
#[inline]
pub fn for_lint() -> Self {
Self::with_options(AnalyzerOptions::for_lint())
}
#[inline]
pub fn for_compile() -> Self {
Self::with_options(AnalyzerOptions::for_compile())
}
pub fn analyze_script(&mut self, source: &str) -> &mut Self {
self.analyze_script_setup(source)
}
pub fn analyze_script_setup(&mut self, source: &str) -> &mut Self {
self.analyze_script_setup_with_generic(source, None)
}
pub fn analyze_script_setup_with_generic(
&mut self,
source: &str,
generic: Option<&str>,
) -> &mut Self {
if !self.options.analyze_script {
return self;
}
self.script_analyzed = true;
let result = profile!(
"croquis.analyzer.script_setup",
crate::script_parser::parse_script_setup_with_generic(source, generic)
);
self.summary.bindings = result.bindings;
self.summary.macros = result.macros;
self.summary.reactivity = result.reactivity;
self.summary.type_exports = result.type_exports;
self.summary.invalid_exports = result.invalid_exports;
self.summary.scopes = result.scopes;
self.summary.provide_inject = result.provide_inject;
self.summary.import_statements = result.import_statements;
self.summary.re_exports = result.re_exports;
self.summary.binding_spans = result.binding_spans;
self.summary.setup_context = result.setup_context;
self
}
pub fn analyze_script_plain(&mut self, source: &str) -> &mut Self {
if !self.options.analyze_script {
return self;
}
self.script_analyzed = true;
let result = profile!(
"croquis.analyzer.script_plain",
crate::script_parser::parse_script(source)
);
self.summary.bindings = result.bindings;
self.summary.macros = result.macros;
self.summary.reactivity = result.reactivity;
self.summary.type_exports = result.type_exports;
self.summary.invalid_exports = result.invalid_exports;
self.summary.scopes = result.scopes;
self.summary.provide_inject = result.provide_inject;
self.summary.import_statements = result.import_statements;
self.summary.re_exports = result.re_exports;
self.summary.binding_spans = result.binding_spans;
self.summary.setup_context = result.setup_context;
self
}
#[inline]
pub fn finish(self) -> Croquis {
profile!("croquis.analyzer.finish", self.summary)
}
#[inline]
pub fn summary(&self) -> &Croquis {
&self.summary
}
#[inline]
pub fn croquis_mut(&mut self) -> &mut Croquis {
&mut self.summary
}
}
impl Default for Analyzer {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::{Analyzer, AnalyzerOptions};
use crate::analysis::{InvalidExportKind, TypeExportKind};
use vize_carton::append;
#[test]
fn test_analyzer_script_bindings() {
let mut analyzer = Analyzer::for_lint();
analyzer.analyze_script(
r#"
const count = ref(0)
const name = 'hello'
let flag = true
function handleClick() {}
"#,
);
let summary = analyzer.finish();
assert!(summary.reactivity.is_reactive("count"));
assert!(summary.reactivity.needs_value_access("count"));
insta::assert_debug_snapshot!(summary);
}
#[test]
fn test_analyzer_define_props() {
let mut analyzer = Analyzer::for_lint();
analyzer.analyze_script(
r#"
const props = defineProps<{
msg: string
count?: number
}>()
"#,
);
let summary = analyzer.finish();
assert_eq!(summary.macros.props().len(), 2);
let prop_names: Vec<_> = summary
.macros
.props()
.iter()
.map(|p| p.name.as_str())
.collect();
assert!(prop_names.contains(&"msg"));
assert!(prop_names.contains(&"count"));
}
#[test]
fn test_type_exports() {
let mut analyzer = Analyzer::for_lint();
analyzer.analyze_script(
r#"
export type Props = {
msg: string
}
export interface Emits {
(e: 'update', value: string): void
}
const count = ref(0)
"#,
);
let summary = analyzer.finish();
assert_eq!(summary.type_exports.len(), 2);
let type_export = &summary.type_exports[0];
assert_eq!(type_export.name.as_str(), "Props");
assert_eq!(type_export.kind, TypeExportKind::Type);
assert!(type_export.hoisted);
let interface_export = &summary.type_exports[1];
assert_eq!(interface_export.name.as_str(), "Emits");
assert_eq!(interface_export.kind, TypeExportKind::Interface);
assert!(interface_export.hoisted);
}
#[test]
fn test_invalid_exports() {
let mut analyzer = Analyzer::for_lint();
analyzer.analyze_script(
r#"
export const foo = 'bar'
export let count = 0
export function hello() {}
export class MyClass {}
export default { foo: 'bar' }
const valid = ref(0)
"#,
);
let summary = analyzer.finish();
assert_eq!(summary.invalid_exports.len(), 5);
let kinds: Vec<_> = summary.invalid_exports.iter().map(|e| e.kind).collect();
assert!(kinds.contains(&InvalidExportKind::Const));
assert!(kinds.contains(&InvalidExportKind::Let));
assert!(kinds.contains(&InvalidExportKind::Function));
assert!(kinds.contains(&InvalidExportKind::Class));
assert!(kinds.contains(&InvalidExportKind::Default));
let names: Vec<_> = summary
.invalid_exports
.iter()
.map(|e| e.name.as_str())
.collect();
assert!(names.contains(&"foo"));
assert!(names.contains(&"count"));
assert!(names.contains(&"hello"));
assert!(names.contains(&"MyClass"));
}
#[test]
fn test_mixed_exports() {
let mut analyzer = Analyzer::for_lint();
analyzer.analyze_script(
r#"
export type MyType = string
export const invalid = 123
export interface MyInterface { name: string }
"#,
);
let summary = analyzer.finish();
assert_eq!(summary.type_exports.len(), 2);
assert_eq!(summary.invalid_exports.len(), 1);
assert_eq!(summary.invalid_exports[0].name.as_str(), "invalid");
}
#[test]
fn test_inject_detection_in_script_setup() {
let mut analyzer = Analyzer::with_options(AnalyzerOptions::full());
analyzer.analyze_script_setup(
r#"import { inject } from 'vue'
const theme = inject('theme')
const { name } = inject('user') as { name: string; id: number }"#,
);
let summary = analyzer.finish();
let injects = summary.provide_inject.injects();
assert_eq!(injects.len(), 2, "Should detect 2 inject calls");
assert_eq!(
injects[0].key,
crate::provide::ProvideKey::String(vize_carton::CompactString::new("theme"))
);
assert_eq!(
injects[1].key,
crate::provide::ProvideKey::String(vize_carton::CompactString::new("user"))
);
assert!(
matches!(
&injects[1].pattern,
crate::provide::InjectPattern::ObjectDestructure(_)
),
"Should detect object destructure pattern"
);
}
#[test]
fn test_full_analysis_snapshot() {
use insta::assert_snapshot;
let mut analyzer = Analyzer::with_options(AnalyzerOptions::full());
analyzer.analyze_script(
r#"import { ref, computed, inject, provide } from 'vue'
import MyComponent from './MyComponent.vue'
const props = defineProps<{
msg: string
count?: number
}>()
const emit = defineEmits<{
(e: 'update', value: string): void
(e: 'delete'): void
}>()
const model = defineModel<string>()
const counter = ref(0)
const doubled = computed(() => counter.value * 2)
const theme = inject('theme')
provide('counter', counter)
function increment() {
counter.value++
emit('update', String(counter.value))
}
export type UserProps = { name: string }
"#,
);
let summary = analyzer.finish();
let mut output = String::new();
output.push_str("=== Bindings ===\n");
for (name, ty) in summary.bindings.iter() {
append!(output, " {name}: {:?}\n", ty);
}
output.push_str("\n=== Macros ===\n");
append!(output, " props: {}\n", summary.macros.props().len());
append!(output, " emits: {}\n", summary.macros.emits().len());
append!(output, " models: {}\n", summary.macros.models().len());
output.push_str("\n=== Reactivity ===\n");
for source in summary.reactivity.sources() {
append!(
output,
" {}: kind={:?}, needs_value={}\n",
source.name,
source.kind,
source.kind.needs_value_access()
);
}
output.push_str("\n=== Provide/Inject ===\n");
append!(
output,
" provides: {}\n",
summary.provide_inject.provides().len()
);
append!(
output,
" injects: {}\n",
summary.provide_inject.injects().len()
);
output.push_str("\n=== Type Exports ===\n");
for te in &summary.type_exports {
append!(output, " {}: {:?}\n", te.name, te.kind);
}
assert_snapshot!(output);
}
#[test]
fn test_props_emits_snapshot() {
use insta::assert_snapshot;
let mut analyzer = Analyzer::for_lint();
analyzer.analyze_script(
r#"
const props = defineProps({
title: String,
count: { type: Number, required: true },
items: { type: Array, default: () => [] }
})
const emit = defineEmits(['update', 'delete', 'select'])
"#,
);
let summary = analyzer.finish();
let mut output = String::new();
output.push_str("=== Props ===\n");
for prop in summary.macros.props() {
append!(
output,
" {}: required={}, has_default={}\n",
prop.name,
prop.required,
prop.default_value.is_some()
);
}
output.push_str("\n=== Emits ===\n");
for emit in summary.macros.emits() {
append!(output, " {}\n", emit.name);
}
assert_snapshot!(output);
}
#[test]
fn test_provide_inject_snapshot() {
use insta::assert_snapshot;
let mut analyzer = Analyzer::with_options(AnalyzerOptions::full());
analyzer.analyze_script(
r#"import { provide, inject } from 'vue'
// Simple provide
provide('theme', 'dark')
// Provide with ref
const counter = ref(0)
provide('counter', counter)
// Provide with Symbol key
const KEY = Symbol('key')
provide(KEY, { value: 42 })
// Simple inject
const theme = inject('theme')
// Inject with default
const locale = inject('locale', 'en')
// Inject with destructure
const { name, id } = inject('user') as { name: string; id: number }
"#,
);
let summary = analyzer.finish();
let mut output = String::new();
output.push_str("=== Provides ===\n");
for p in summary.provide_inject.provides() {
append!(output, " key: {:?}\n", p.key);
}
output.push_str("\n=== Injects ===\n");
for i in summary.provide_inject.injects() {
append!(
output,
" key: {:?}, has_default: {}, pattern: {:?}\n",
i.key,
i.default_value.is_some(),
i.pattern
);
}
assert_snapshot!(output);
}
#[test]
fn test_vif_guard_in_template() {
use vize_armature::parse;
use vize_carton::Bump;
let allocator = Bump::new();
let template = r#"<div>
<p v-if="todo.description">{{ unwrapDescription(todo.description) }}</p>
<span>{{ todo.title }}</span>
</div>"#;
let (root, errors) = parse(&allocator, template);
assert!(errors.is_empty(), "Template should parse without errors");
let mut analyzer = Analyzer::with_options(AnalyzerOptions::full());
analyzer.analyze_template(&root);
let summary = analyzer.finish();
let expressions: Vec<_> = summary
.template_expressions
.iter()
.filter(|e| {
matches!(
e.kind,
crate::analysis::TemplateExpressionKind::Interpolation
)
})
.collect();
insta::assert_debug_snapshot!(expressions);
}
}