Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions crates/oxc_linter/src/generated/rule_runner_impls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2440,3 +2440,7 @@ impl RuleRunner for crate::rules::vitest::require_local_test_context_for_concurr
impl RuleRunner for crate::rules::vue::valid_define_emits::ValidDefineEmits {
const NODE_TYPES: Option<&AstTypesBitset> = None;
}

impl RuleRunner for crate::rules::vue::valid_define_props::ValidDefineProps {
const NODE_TYPES: Option<&AstTypesBitset> = None;
}
2 changes: 2 additions & 0 deletions crates/oxc_linter/src/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -625,6 +625,7 @@ pub(crate) mod node {

pub(crate) mod vue {
pub mod valid_define_emits;
pub mod valid_define_props;
}

oxc_macros::declare_all_lint_rules! {
Expand Down Expand Up @@ -1203,4 +1204,5 @@ oxc_macros::declare_all_lint_rules! {
vitest::prefer_to_be_truthy,
vitest::require_local_test_context_for_concurrent_snapshots,
vue::valid_define_emits,
vue::valid_define_props,
}
123 changes: 22 additions & 101 deletions crates/oxc_linter/src/rules/vue/valid_define_emits.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
use oxc_ast::{
AstKind,
ast::{
CallExpression, ExportDefaultDeclarationKind, Expression, IdentifierReference,
ObjectPropertyKind,
},
};
use oxc_ast::AstKind;
use oxc_diagnostics::OxcDiagnostic;
use oxc_macros::declare_oxc_lint;
use oxc_span::Span;

use crate::{ContextSubHost, context::LintContext, frameworks::FrameworkOptions, rule::Rule};
use crate::{
context::LintContext,
frameworks::FrameworkOptions,
rule::Rule,
utils::{DefineMacroProblem, check_define_macro_call_expression, has_default_exports_property},
};

fn has_type_and_arguments_diagnostic(span: Span) -> OxcDiagnostic {
OxcDiagnostic::warn("`defineEmits` has both a type-only emit and an argument.")
Expand Down Expand Up @@ -140,7 +139,7 @@ impl Rule for ValidDefineEmits {
fn run_once(&self, ctx: &LintContext) {
let mut found: Option<Span> = None;

let has_other_script_emits = has_default_emits_exports(&ctx.other_file_hosts());
let has_other_script_emits = has_default_exports_property(&ctx.other_file_hosts(), "emits");
for node in ctx.nodes() {
let AstKind::CallExpression(call_expr) = node.kind() else {
continue;
Expand All @@ -161,105 +160,27 @@ impl Rule for ValidDefineEmits {
}
found = Some(call_expr.span);

handle_call_expression(call_expr, ctx, has_other_script_emits);
}
}

fn should_run(&self, ctx: &crate::context::ContextHost) -> bool {
ctx.frameworks_options() == FrameworkOptions::VueSetup
}
}

fn handle_call_expression(
call_expr: &CallExpression,
ctx: &LintContext,
has_other_script_emits: bool,
) {
let has_type_args = call_expr.type_arguments.is_some();

if has_type_args && has_other_script_emits {
ctx.diagnostic(define_in_both(call_expr.span));
return;
}

// `defineEmits` has type arguments and js arguments. Vue Compiler allows only one of them.
if has_type_args && !call_expr.arguments.is_empty() {
ctx.diagnostic(has_type_and_arguments_diagnostic(call_expr.span));
return; // Skip if there are type arguments
}

if has_type_args {
// If there are type arguments, we don't need to check the arguments.
return;
}

let Some(expression) = call_expr.arguments.first().and_then(|first| first.as_expression())
else {
// `defineEmits();` is valid when `export default { emits: [] }` is defined
if !has_other_script_emits {
ctx.diagnostic(events_not_defined(call_expr.span));
}
return;
};

if has_other_script_emits {
ctx.diagnostic(define_in_both(call_expr.span));
return;
}

match expression {
Expression::ArrayExpression(_) | Expression::ObjectExpression(_) => {}
Expression::Identifier(identifier) => {
if !is_non_local_reference(identifier, ctx) {
ctx.diagnostic(referencing_locally(call_expr.span));
}
}
_ => {
ctx.diagnostic(referencing_locally(call_expr.span));
}
}
}

pub fn is_non_local_reference(identifier: &IdentifierReference, ctx: &LintContext<'_>) -> bool {
if let Some(symbol_id) = ctx.semantic().scoping().get_root_binding(&identifier.name) {
return matches!(
ctx.semantic().symbol_declaration(symbol_id).kind(),
AstKind::ImportSpecifier(_)
);
}

// variables outside the current `<script>` block are valid.
// This is the same for unresolved variables.
true
}

fn has_default_emits_exports(others: &Vec<&ContextSubHost<'_>>) -> bool {
for host in others {
for other_node in host.semantic().nodes() {
let AstKind::ExportDefaultDeclaration(export) = other_node.kind() else {
continue;
};

let ExportDefaultDeclarationKind::ObjectExpression(export_obj) = &export.declaration
let Some(problem) =
check_define_macro_call_expression(call_expr, ctx, has_other_script_emits)
else {
continue;
};

let has_emits_exports = export_obj.properties.iter().any(|property| {
let ObjectPropertyKind::ObjectProperty(property) = property else {
return false;
};

property.key.name().is_some_and(|name| name == "emits")
});

if has_emits_exports {
return true;
}
let diagnostic = match problem {
DefineMacroProblem::DefineInBoth => define_in_both(call_expr.span),
DefineMacroProblem::HasTypeAndArguments => {
has_type_and_arguments_diagnostic(call_expr.span)
}
DefineMacroProblem::EventsNotDefined => events_not_defined(call_expr.span),
DefineMacroProblem::ReferencingLocally => referencing_locally(call_expr.span),
};
ctx.diagnostic(diagnostic);
}
}

false
fn should_run(&self, ctx: &crate::context::ContextHost) -> bool {
ctx.frameworks_options() == FrameworkOptions::VueSetup
}
}

#[test]
Expand Down
Loading
Loading