use crate::context::LintContext;
use crate::diagnostic::{LintDiagnostic, Severity};
use crate::rule::{Rule, RuleCategory, RuleMeta};
use vize_carton::String;
use vize_carton::ToCompactString;
use vize_croquis::builtins::is_builtin_component;
use vize_relief::ast::{ElementNode, RootNode};
static META: RuleMeta = RuleMeta {
name: "vue/require-component-registration",
description: "Require explicit import or registration for components",
category: RuleCategory::Recommended,
fixable: false,
default_severity: Severity::Warning,
};
const FRAMEWORK_GLOBALS: &[&str] = &[
"nuxt-link",
"nuxt",
"nuxt-child",
"nuxt-page",
"client-only",
"nuxt-loading-indicator",
"nuxt-layout",
"nuxt-error-boundary",
"router-link",
"router-view",
];
#[derive(Default)]
pub struct RequireComponentRegistration {
pub ignore_globals: Vec<String>,
pub nuxt_mode: bool,
}
impl RequireComponentRegistration {
pub fn nuxt() -> Self {
Self {
ignore_globals: Vec::new(),
nuxt_mode: true,
}
}
fn is_custom_component(&self, tag: &str) -> bool {
let first_char = tag.chars().next().unwrap_or('a');
if first_char.is_uppercase() {
return true;
}
if tag.contains('-') {
let is_web_component = tag.starts_with("x-")
|| tag.starts_with("ion-")
|| tag.starts_with("md-")
|| tag.starts_with("mwc-");
return !is_web_component;
}
false
}
fn is_builtin(&self, tag: &str) -> bool {
if is_builtin_component(tag) {
return true;
}
let lower = tag.to_lowercase();
is_builtin_component(&lower)
}
fn is_framework_global(&self, tag: &str) -> bool {
let lower = tag.to_lowercase();
let kebab = pascal_to_kebab(tag);
FRAMEWORK_GLOBALS.contains(&lower.as_str())
|| FRAMEWORK_GLOBALS.contains(&kebab.as_str())
|| self
.ignore_globals
.iter()
.any(|g| g.eq_ignore_ascii_case(tag) || g.eq_ignore_ascii_case(&kebab))
}
}
impl Rule for RequireComponentRegistration {
fn meta(&self) -> &'static RuleMeta {
&META
}
fn run_on_template<'a>(&self, ctx: &mut LintContext<'a>, root: &RootNode<'a>) {
let mut used_components: Vec<(String, u32, u32)> = Vec::new();
collect_components(root, &mut used_components);
for (tag, start, end) in used_components {
if self.is_custom_component(&tag)
&& !self.is_builtin(&tag)
&& !self.is_framework_global(&tag)
{
if self.nuxt_mode {
continue;
}
ctx.report(
LintDiagnostic::warn(
META.name,
"Component is used but not explicitly imported",
start,
end,
)
.with_help("Import the component in <script setup> or register it in components option"),
);
}
}
}
}
fn collect_components<'a>(root: &RootNode<'a>, result: &mut Vec<(String, u32, u32)>) {
fn visit_element<'a>(element: &ElementNode<'a>, result: &mut Vec<(String, u32, u32)>) {
let start = element.loc.start.offset;
let tag_str = element.tag.as_str();
result.push((
tag_str.to_compact_string(),
start,
start + tag_str.len() as u32,
));
for child in element.children.iter() {
if let vize_relief::ast::TemplateChildNode::Element(el) = child {
visit_element(el, result);
}
}
}
for child in root.children.iter() {
if let vize_relief::ast::TemplateChildNode::Element(el) = child {
visit_element(el, result);
}
}
}
fn pascal_to_kebab(s: &str) -> String {
let mut result = String::with_capacity(s.len() + 4);
for (i, c) in s.chars().enumerate() {
if c.is_uppercase() {
if i > 0 {
result.push('-');
}
result.push(c.to_ascii_lowercase());
} else {
result.push(c);
}
}
result
}
#[cfg(test)]
mod tests {
use super::{pascal_to_kebab, RequireComponentRegistration};
#[test]
fn test_pascal_to_kebab() {
assert_eq!(pascal_to_kebab("MyButton"), "my-button");
assert_eq!(pascal_to_kebab("NuxtLink"), "nuxt-link");
assert_eq!(pascal_to_kebab("RouterView"), "router-view");
}
#[test]
fn test_is_custom_component() {
let rule = RequireComponentRegistration::default();
assert!(rule.is_custom_component("MyButton"));
assert!(rule.is_custom_component("my-button"));
assert!(!rule.is_custom_component("div"));
assert!(!rule.is_custom_component("span"));
}
#[test]
fn test_is_builtin() {
let rule = RequireComponentRegistration::default();
assert!(rule.is_builtin("component"));
assert!(rule.is_builtin("Transition"));
assert!(rule.is_builtin("keep-alive"));
assert!(!rule.is_builtin("MyButton"));
}
}