mod config;
mod engine;
pub use config::{LintResult, Linter};
#[cfg(test)]
mod tests {
use super::Linter;
use vize_carton::{Allocator, ToCompactString};
#[test]
fn test_lint_empty_template() {
let linter = Linter::new();
let result = linter.lint_template("", "test.vue");
assert!(!result.has_errors());
assert!(!result.has_diagnostics());
}
#[test]
fn test_lint_simple_template() {
let linter = Linter::new();
let result = linter.lint_template("<div>Hello</div>", "test.vue");
assert!(!result.has_errors());
}
#[test]
fn test_lint_with_allocator_reuse() {
let linter = Linter::new();
let allocator = Allocator::with_capacity(1024);
let result1 =
linter.lint_template_with_allocator(&allocator, "<div>Hello</div>", "test1.vue");
assert!(!result1.has_errors());
}
#[test]
fn test_lint_files_batch() {
let linter = Linter::new();
let files = vec![
(
"test1.vue".to_compact_string(),
"<div>Hello</div>".to_compact_string(),
),
(
"test2.vue".to_compact_string(),
"<span>World</span>".to_compact_string(),
),
];
let (results, summary) = linter.lint_files(&files);
assert_eq!(results.len(), 2);
assert_eq!(summary.file_count, 2);
}
#[test]
fn test_vize_forget_suppresses_next_element() {
let linter = Linter::new();
let result = linter.lint_template(
r#"<ul><li v-for="item in items">{{ item }}</li></ul>"#,
"test.vue",
);
assert!(result.error_count > 0, "Should have error without key");
let result = linter.lint_template(
r#"<ul><!-- @vize:forget v-for key not needed here -->
<li v-for="item in items">{{ item }}</li></ul>"#,
"test.vue",
);
assert_eq!(
result.error_count, 0,
"Error should be suppressed by @vize:forget"
);
}
#[test]
fn test_vize_forget_without_reason_warns() {
let linter = Linter::new();
let result = linter.lint_template(
r#"<ul><!-- @vize:forget -->
<li v-for="item in items">{{ item }}</li></ul>"#,
"test.vue",
);
assert_eq!(result.error_count, 0, "v-for error should be suppressed");
assert_eq!(result.warning_count, 1, "Should warn about missing reason");
assert_eq!(result.diagnostics[0].rule_name, "vize/forget");
}
#[test]
fn test_vize_forget_multiline_element() {
let linter = Linter::new();
let result = linter.lint_template(
r#"<ul><!-- @vize:forget complex rendering -->
<li
v-for="item in items"
class="item"
>{{ item }}</li></ul>"#,
"test.vue",
);
assert_eq!(
result.error_count, 0,
"Multiline element should be fully suppressed"
);
}
#[test]
fn test_vize_forget_suppresses_template_v_for() {
let linter = Linter::new();
let result = linter.lint_template(
r#"<div><!-- @vize:forget template key is valid in Vue 3 -->
<template v-for="item in items" :key="item.id">
<li>{{ item.name }}</li>
</template></div>"#,
"test.vue",
);
assert_eq!(
result.error_count, 0,
"ForNode should be suppressed by @vize:forget"
);
}
#[test]
fn test_vize_forget_suppresses_template_v_if() {
let linter = Linter::new();
let result = linter.lint_template(
r#"<div><!-- @vize:forget conditional rendering -->
<span v-if="show" v-for="item in items">{{ item }}</span></div>"#,
"test.vue",
);
assert_eq!(
result.error_count, 0,
"IfNode should be suppressed by @vize:forget"
);
}
#[test]
fn test_lint_sfc_extracts_template() {
let linter = Linter::new();
let sfc = r#"<script setup lang="ts">
interface Props {
schema?: BaseSchema<FormShape, FormShape, any>;
}
</script>
<template>
<div>Hello World</div>
</template>
"#;
let result = linter.lint_sfc(sfc, "test.vue");
assert_eq!(result.error_count, 0);
assert_eq!(result.warning_count, 0);
}
#[test]
fn test_lint_sfc_no_template() {
let linter = Linter::new();
let sfc = r#"<script setup lang="ts">
const foo = 'bar';
</script>
"#;
let result = linter.lint_sfc(sfc, "test.vue");
assert_eq!(result.error_count, 0);
assert_eq!(result.warning_count, 0);
}
#[test]
fn test_lint_sfc_byte_offset() {
let linter = Linter::new();
let sfc = r#"<script setup lang="ts">
const foo = 'bar';
</script>
<template>
<ul><li v-for="item in items">{{ item }}</li></ul>
</template>
"#;
let result = linter.lint_sfc(sfc, "test.vue");
assert!(result.error_count > 0, "Should detect v-for without key");
if let Some(diag) = result.diagnostics.first() {
assert!(
diag.start > 50,
"Byte offset should be adjusted for template position"
);
}
}
#[test]
fn test_lint_sfc_offset_line_conversion() {
use crate::telegraph::LspEmitter;
let linter = Linter::new();
let sfc = r#"<script setup lang="ts">
const foo = 'bar';
</script>
<template>
<ul><li v-for="item in items">{{ item }}</li></ul>
</template>
"#;
let result = linter.lint_sfc(sfc, "test.vue");
assert!(result.error_count > 0);
let template_start = sfc.find("<template>").unwrap();
eprintln!("Template <template> starts at byte: {}", template_start);
let content_start = sfc.find("<template>").unwrap() + "<template>\n".len();
eprintln!("Template content starts at byte: {}", content_start);
for (i, diag) in result.diagnostics.iter().enumerate() {
eprintln!(
"Diag[{}] rule={}, start={}, end={}",
i, diag.rule_name, diag.start, diag.end
);
let before = &sfc[..diag.start as usize];
let line_count = before.matches('\n').count();
eprintln!(" -> Line (0-indexed): {}", line_count);
}
let lsp_diags = LspEmitter::to_lsp_diagnostics_with_source(&result, sfc);
for (i, lsp) in lsp_diags.iter().enumerate() {
eprintln!(
"LSP[{}] line={}, col={}",
i, lsp.range.start.line, lsp.range.start.character
);
}
if let Some(lsp) = lsp_diags.first() {
assert_eq!(
lsp.range.start.line, 5,
"First diagnostic should be on line 5 (0-indexed)"
);
}
}
#[test]
fn test_lint_sfc_with_nested_templates() {
let linter = Linter::new();
let sfc = r#"<script setup lang="ts">
const show = true;
</script>
<template>
<div>
<template v-if="show">
<span>Visible</span>
</template>
<template v-else>
<span>Hidden</span>
</template>
</div>
</template>
"#;
let result = linter.lint_sfc(sfc, "test.vue");
assert_eq!(
result.error_count, 0,
"Should not report errors for valid nested templates with directives"
);
}
#[test]
fn test_lint_sfc_with_nested_template_extraction() {
let linter = Linter::new();
let sfc = r#"<script></script>
<template>
<div>
<template v-if="x">nested</template>
</div>
</template>"#;
let result = linter.lint_sfc(sfc, "test.vue");
assert_eq!(
result.error_count, 0,
"Should properly extract and lint nested templates"
);
}
#[test]
fn test_vize_todo_emits_warning() {
let linter = Linter::new();
let result = linter.lint_template(
r#"<div><!-- @vize:todo fix this --><span>hello</span></div>"#,
"test.vue",
);
assert_eq!(
result.warning_count, 1,
"Should emit 1 warning for @vize:todo"
);
assert_eq!(result.diagnostics[0].rule_name, "vize/todo");
assert!(result.diagnostics[0].message.contains("TODO"));
}
#[test]
fn test_vize_fixme_emits_error() {
let linter = Linter::new();
let result = linter.lint_template(
r#"<div><!-- @vize:fixme broken --><span>hello</span></div>"#,
"test.vue",
);
assert_eq!(result.error_count, 1, "Should emit 1 error for @vize:fixme");
assert_eq!(result.diagnostics[0].rule_name, "vize/fixme");
assert!(result.diagnostics[0].message.contains("FIXME"));
}
#[test]
fn test_vize_deprecated_emits_warning() {
let linter = Linter::new();
let result = linter.lint_template(
r#"<div><!-- @vize:deprecated use NewComp --><span>hello</span></div>"#,
"test.vue",
);
assert_eq!(
result.warning_count, 1,
"Should emit 1 warning for @vize:deprecated"
);
assert_eq!(result.diagnostics[0].rule_name, "vize/deprecated");
assert!(result.diagnostics[0].message.contains("Deprecated"));
}
#[test]
fn test_vize_expected_suppresses_error() {
let linter = Linter::new();
let result = linter.lint_template(
r#"<ul><li v-for="item in items">{{ item }}</li></ul>"#,
"test.vue",
);
assert!(result.error_count > 0, "Should have error without key");
let result = linter.lint_template(
r#"<ul><!-- @vize:expected -->
<li v-for="item in items">{{ item }}</li></ul>"#,
"test.vue",
);
assert_eq!(
result.error_count, 0,
"Error should be suppressed by @vize:expected"
);
}
#[test]
fn test_vize_ignore_start_end_region() {
let linter = Linter::new();
let result = linter.lint_template(
r#"<!-- @vize:ignore-start -->
<ul><li v-for="item in items">{{ item }}</li></ul>
<!-- @vize:ignore-end -->"#,
"test.vue",
);
assert_eq!(
result.error_count, 0,
"Errors in ignore region should be suppressed"
);
}
#[test]
fn test_vize_docs_no_lint_effect() {
let linter = Linter::new();
let result = linter.lint_template(
r#"<div><!-- @vize:docs Component documentation --><span>hello</span></div>"#,
"test.vue",
);
assert_eq!(
result.error_count, 0,
"Docs directive should not produce errors"
);
assert_eq!(
result.warning_count, 0,
"Docs directive should not produce warnings"
);
}
}