diff --git a/crates/oxc_linter/src/generated/rule_runner_impls.rs b/crates/oxc_linter/src/generated/rule_runner_impls.rs index d5c2e84376e1f..eedb27f7fe394 100644 --- a/crates/oxc_linter/src/generated/rule_runner_impls.rs +++ b/crates/oxc_linter/src/generated/rule_runner_impls.rs @@ -3270,6 +3270,14 @@ impl RuleRunner for crate::rules::unicorn::prefer_global_this::PreferGlobalThis const RUN_FUNCTIONS: RuleRunFunctionsImplemented = RuleRunFunctionsImplemented::Run; } +impl RuleRunner + for crate::rules::unicorn::prefer_import_meta_properties::PreferImportMetaProperties +{ + const NODE_TYPES: Option<&AstTypesBitset> = + Some(&AstTypesBitset::from_types(&[AstType::MetaProperty])); + const RUN_FUNCTIONS: RuleRunFunctionsImplemented = RuleRunFunctionsImplemented::Run; +} + impl RuleRunner for crate::rules::unicorn::prefer_includes::PreferIncludes { const NODE_TYPES: Option<&AstTypesBitset> = Some(&AstTypesBitset::from_types(&[AstType::BinaryExpression])); diff --git a/crates/oxc_linter/src/generated/rules_enum.rs b/crates/oxc_linter/src/generated/rules_enum.rs index b74afeab1b5c5..137f34678419b 100644 --- a/crates/oxc_linter/src/generated/rules_enum.rs +++ b/crates/oxc_linter/src/generated/rules_enum.rs @@ -634,6 +634,7 @@ pub use crate::rules::unicorn::prefer_dom_node_remove::PreferDomNodeRemove as Un pub use crate::rules::unicorn::prefer_dom_node_text_content::PreferDomNodeTextContent as UnicornPreferDomNodeTextContent; pub use crate::rules::unicorn::prefer_event_target::PreferEventTarget as UnicornPreferEventTarget; pub use crate::rules::unicorn::prefer_global_this::PreferGlobalThis as UnicornPreferGlobalThis; +pub use crate::rules::unicorn::prefer_import_meta_properties::PreferImportMetaProperties as UnicornPreferImportMetaProperties; pub use crate::rules::unicorn::prefer_includes::PreferIncludes as UnicornPreferIncludes; pub use crate::rules::unicorn::prefer_keyboard_event_key::PreferKeyboardEventKey as UnicornPreferKeyboardEventKey; pub use crate::rules::unicorn::prefer_logical_operator_over_ternary::PreferLogicalOperatorOverTernary as UnicornPreferLogicalOperatorOverTernary; @@ -1228,6 +1229,7 @@ pub enum RuleEnum { UnicornPreferDomNodeTextContent(UnicornPreferDomNodeTextContent), UnicornPreferEventTarget(UnicornPreferEventTarget), UnicornPreferGlobalThis(UnicornPreferGlobalThis), + UnicornPreferImportMetaProperties(UnicornPreferImportMetaProperties), UnicornPreferIncludes(UnicornPreferIncludes), UnicornPreferKeyboardEventKey(UnicornPreferKeyboardEventKey), UnicornPreferLogicalOperatorOverTernary(UnicornPreferLogicalOperatorOverTernary), @@ -1995,7 +1997,8 @@ const UNICORN_PREFER_DOM_NODE_REMOVE_ID: usize = UNICORN_PREFER_DOM_NODE_DATASET const UNICORN_PREFER_DOM_NODE_TEXT_CONTENT_ID: usize = UNICORN_PREFER_DOM_NODE_REMOVE_ID + 1usize; const UNICORN_PREFER_EVENT_TARGET_ID: usize = UNICORN_PREFER_DOM_NODE_TEXT_CONTENT_ID + 1usize; const UNICORN_PREFER_GLOBAL_THIS_ID: usize = UNICORN_PREFER_EVENT_TARGET_ID + 1usize; -const UNICORN_PREFER_INCLUDES_ID: usize = UNICORN_PREFER_GLOBAL_THIS_ID + 1usize; +const UNICORN_PREFER_IMPORT_META_PROPERTIES_ID: usize = UNICORN_PREFER_GLOBAL_THIS_ID + 1usize; +const UNICORN_PREFER_INCLUDES_ID: usize = UNICORN_PREFER_IMPORT_META_PROPERTIES_ID + 1usize; const UNICORN_PREFER_KEYBOARD_EVENT_KEY_ID: usize = UNICORN_PREFER_INCLUDES_ID + 1usize; const UNICORN_PREFER_LOGICAL_OPERATOR_OVER_TERNARY_ID: usize = UNICORN_PREFER_KEYBOARD_EVENT_KEY_ID + 1usize; @@ -2811,6 +2814,7 @@ impl RuleEnum { Self::UnicornPreferDomNodeTextContent(_) => UNICORN_PREFER_DOM_NODE_TEXT_CONTENT_ID, Self::UnicornPreferEventTarget(_) => UNICORN_PREFER_EVENT_TARGET_ID, Self::UnicornPreferGlobalThis(_) => UNICORN_PREFER_GLOBAL_THIS_ID, + Self::UnicornPreferImportMetaProperties(_) => UNICORN_PREFER_IMPORT_META_PROPERTIES_ID, Self::UnicornPreferIncludes(_) => UNICORN_PREFER_INCLUDES_ID, Self::UnicornPreferKeyboardEventKey(_) => UNICORN_PREFER_KEYBOARD_EVENT_KEY_ID, Self::UnicornPreferLogicalOperatorOverTernary(_) => { @@ -3614,6 +3618,7 @@ impl RuleEnum { Self::UnicornPreferDomNodeTextContent(_) => UnicornPreferDomNodeTextContent::NAME, Self::UnicornPreferEventTarget(_) => UnicornPreferEventTarget::NAME, Self::UnicornPreferGlobalThis(_) => UnicornPreferGlobalThis::NAME, + Self::UnicornPreferImportMetaProperties(_) => UnicornPreferImportMetaProperties::NAME, Self::UnicornPreferIncludes(_) => UnicornPreferIncludes::NAME, Self::UnicornPreferKeyboardEventKey(_) => UnicornPreferKeyboardEventKey::NAME, Self::UnicornPreferLogicalOperatorOverTernary(_) => { @@ -4449,6 +4454,9 @@ impl RuleEnum { Self::UnicornPreferDomNodeTextContent(_) => UnicornPreferDomNodeTextContent::CATEGORY, Self::UnicornPreferEventTarget(_) => UnicornPreferEventTarget::CATEGORY, Self::UnicornPreferGlobalThis(_) => UnicornPreferGlobalThis::CATEGORY, + Self::UnicornPreferImportMetaProperties(_) => { + UnicornPreferImportMetaProperties::CATEGORY + } Self::UnicornPreferIncludes(_) => UnicornPreferIncludes::CATEGORY, Self::UnicornPreferKeyboardEventKey(_) => UnicornPreferKeyboardEventKey::CATEGORY, Self::UnicornPreferLogicalOperatorOverTernary(_) => { @@ -5265,6 +5273,7 @@ impl RuleEnum { Self::UnicornPreferDomNodeTextContent(_) => UnicornPreferDomNodeTextContent::FIX, Self::UnicornPreferEventTarget(_) => UnicornPreferEventTarget::FIX, Self::UnicornPreferGlobalThis(_) => UnicornPreferGlobalThis::FIX, + Self::UnicornPreferImportMetaProperties(_) => UnicornPreferImportMetaProperties::FIX, Self::UnicornPreferIncludes(_) => UnicornPreferIncludes::FIX, Self::UnicornPreferKeyboardEventKey(_) => UnicornPreferKeyboardEventKey::FIX, Self::UnicornPreferLogicalOperatorOverTernary(_) => { @@ -6207,6 +6216,9 @@ impl RuleEnum { } Self::UnicornPreferEventTarget(_) => UnicornPreferEventTarget::documentation(), Self::UnicornPreferGlobalThis(_) => UnicornPreferGlobalThis::documentation(), + Self::UnicornPreferImportMetaProperties(_) => { + UnicornPreferImportMetaProperties::documentation() + } Self::UnicornPreferIncludes(_) => UnicornPreferIncludes::documentation(), Self::UnicornPreferKeyboardEventKey(_) => { UnicornPreferKeyboardEventKey::documentation() @@ -7928,6 +7940,10 @@ impl RuleEnum { .or_else(|| UnicornPreferEventTarget::schema(generator)), Self::UnicornPreferGlobalThis(_) => UnicornPreferGlobalThis::config_schema(generator) .or_else(|| UnicornPreferGlobalThis::schema(generator)), + Self::UnicornPreferImportMetaProperties(_) => { + UnicornPreferImportMetaProperties::config_schema(generator) + .or_else(|| UnicornPreferImportMetaProperties::schema(generator)) + } Self::UnicornPreferIncludes(_) => UnicornPreferIncludes::config_schema(generator) .or_else(|| UnicornPreferIncludes::schema(generator)), Self::UnicornPreferKeyboardEventKey(_) => { @@ -8981,6 +8997,7 @@ impl RuleEnum { Self::UnicornPreferDomNodeTextContent(_) => "unicorn", Self::UnicornPreferEventTarget(_) => "unicorn", Self::UnicornPreferGlobalThis(_) => "unicorn", + Self::UnicornPreferImportMetaProperties(_) => "unicorn", Self::UnicornPreferIncludes(_) => "unicorn", Self::UnicornPreferKeyboardEventKey(_) => "unicorn", Self::UnicornPreferLogicalOperatorOverTernary(_) => "unicorn", @@ -10823,6 +10840,11 @@ impl RuleEnum { Self::UnicornPreferGlobalThis(_) => Ok(Self::UnicornPreferGlobalThis( UnicornPreferGlobalThis::from_configuration(value)?, )), + Self::UnicornPreferImportMetaProperties(_) => { + Ok(Self::UnicornPreferImportMetaProperties( + UnicornPreferImportMetaProperties::from_configuration(value)?, + )) + } Self::UnicornPreferIncludes(_) => { Ok(Self::UnicornPreferIncludes(UnicornPreferIncludes::from_configuration(value)?)) } @@ -11944,6 +11966,7 @@ impl RuleEnum { Self::UnicornPreferDomNodeTextContent(rule) => rule.to_configuration(), Self::UnicornPreferEventTarget(rule) => rule.to_configuration(), Self::UnicornPreferGlobalThis(rule) => rule.to_configuration(), + Self::UnicornPreferImportMetaProperties(rule) => rule.to_configuration(), Self::UnicornPreferIncludes(rule) => rule.to_configuration(), Self::UnicornPreferKeyboardEventKey(rule) => rule.to_configuration(), Self::UnicornPreferLogicalOperatorOverTernary(rule) => rule.to_configuration(), @@ -12651,6 +12674,7 @@ impl RuleEnum { Self::UnicornPreferDomNodeTextContent(rule) => rule.run(node, ctx), Self::UnicornPreferEventTarget(rule) => rule.run(node, ctx), Self::UnicornPreferGlobalThis(rule) => rule.run(node, ctx), + Self::UnicornPreferImportMetaProperties(rule) => rule.run(node, ctx), Self::UnicornPreferIncludes(rule) => rule.run(node, ctx), Self::UnicornPreferKeyboardEventKey(rule) => rule.run(node, ctx), Self::UnicornPreferLogicalOperatorOverTernary(rule) => rule.run(node, ctx), @@ -13356,6 +13380,7 @@ impl RuleEnum { Self::UnicornPreferDomNodeTextContent(rule) => rule.run_once(ctx), Self::UnicornPreferEventTarget(rule) => rule.run_once(ctx), Self::UnicornPreferGlobalThis(rule) => rule.run_once(ctx), + Self::UnicornPreferImportMetaProperties(rule) => rule.run_once(ctx), Self::UnicornPreferIncludes(rule) => rule.run_once(ctx), Self::UnicornPreferKeyboardEventKey(rule) => rule.run_once(ctx), Self::UnicornPreferLogicalOperatorOverTernary(rule) => rule.run_once(ctx), @@ -14147,6 +14172,7 @@ impl RuleEnum { Self::UnicornPreferDomNodeTextContent(rule) => rule.run_on_jest_node(jest_node, ctx), Self::UnicornPreferEventTarget(rule) => rule.run_on_jest_node(jest_node, ctx), Self::UnicornPreferGlobalThis(rule) => rule.run_on_jest_node(jest_node, ctx), + Self::UnicornPreferImportMetaProperties(rule) => rule.run_on_jest_node(jest_node, ctx), Self::UnicornPreferIncludes(rule) => rule.run_on_jest_node(jest_node, ctx), Self::UnicornPreferKeyboardEventKey(rule) => rule.run_on_jest_node(jest_node, ctx), Self::UnicornPreferLogicalOperatorOverTernary(rule) => { @@ -14866,6 +14892,7 @@ impl RuleEnum { Self::UnicornPreferDomNodeTextContent(rule) => rule.should_run(ctx), Self::UnicornPreferEventTarget(rule) => rule.should_run(ctx), Self::UnicornPreferGlobalThis(rule) => rule.should_run(ctx), + Self::UnicornPreferImportMetaProperties(rule) => rule.should_run(ctx), Self::UnicornPreferIncludes(rule) => rule.should_run(ctx), Self::UnicornPreferKeyboardEventKey(rule) => rule.should_run(ctx), Self::UnicornPreferLogicalOperatorOverTernary(rule) => rule.should_run(ctx), @@ -15793,6 +15820,9 @@ impl RuleEnum { } Self::UnicornPreferEventTarget(_) => UnicornPreferEventTarget::IS_TSGOLINT_RULE, Self::UnicornPreferGlobalThis(_) => UnicornPreferGlobalThis::IS_TSGOLINT_RULE, + Self::UnicornPreferImportMetaProperties(_) => { + UnicornPreferImportMetaProperties::IS_TSGOLINT_RULE + } Self::UnicornPreferIncludes(_) => UnicornPreferIncludes::IS_TSGOLINT_RULE, Self::UnicornPreferKeyboardEventKey(_) => { UnicornPreferKeyboardEventKey::IS_TSGOLINT_RULE @@ -16711,6 +16741,9 @@ impl RuleEnum { Self::UnicornPreferDomNodeTextContent(_) => UnicornPreferDomNodeTextContent::HAS_CONFIG, Self::UnicornPreferEventTarget(_) => UnicornPreferEventTarget::HAS_CONFIG, Self::UnicornPreferGlobalThis(_) => UnicornPreferGlobalThis::HAS_CONFIG, + Self::UnicornPreferImportMetaProperties(_) => { + UnicornPreferImportMetaProperties::HAS_CONFIG + } Self::UnicornPreferIncludes(_) => UnicornPreferIncludes::HAS_CONFIG, Self::UnicornPreferKeyboardEventKey(_) => UnicornPreferKeyboardEventKey::HAS_CONFIG, Self::UnicornPreferLogicalOperatorOverTernary(_) => { @@ -17448,6 +17481,7 @@ impl RuleEnum { Self::UnicornPreferDomNodeTextContent(rule) => rule.types_info(), Self::UnicornPreferEventTarget(rule) => rule.types_info(), Self::UnicornPreferGlobalThis(rule) => rule.types_info(), + Self::UnicornPreferImportMetaProperties(rule) => rule.types_info(), Self::UnicornPreferIncludes(rule) => rule.types_info(), Self::UnicornPreferKeyboardEventKey(rule) => rule.types_info(), Self::UnicornPreferLogicalOperatorOverTernary(rule) => rule.types_info(), @@ -18153,6 +18187,7 @@ impl RuleEnum { Self::UnicornPreferDomNodeTextContent(rule) => rule.run_info(), Self::UnicornPreferEventTarget(rule) => rule.run_info(), Self::UnicornPreferGlobalThis(rule) => rule.run_info(), + Self::UnicornPreferImportMetaProperties(rule) => rule.run_info(), Self::UnicornPreferIncludes(rule) => rule.run_info(), Self::UnicornPreferKeyboardEventKey(rule) => rule.run_info(), Self::UnicornPreferLogicalOperatorOverTernary(rule) => rule.run_info(), @@ -18962,6 +18997,7 @@ pub static RULES: std::sync::LazyLock> = std::sync::LazyLock::new( RuleEnum::UnicornPreferDomNodeTextContent(UnicornPreferDomNodeTextContent::default()), RuleEnum::UnicornPreferEventTarget(UnicornPreferEventTarget::default()), RuleEnum::UnicornPreferGlobalThis(UnicornPreferGlobalThis::default()), + RuleEnum::UnicornPreferImportMetaProperties(UnicornPreferImportMetaProperties::default()), RuleEnum::UnicornPreferIncludes(UnicornPreferIncludes::default()), RuleEnum::UnicornPreferKeyboardEventKey(UnicornPreferKeyboardEventKey::default()), RuleEnum::UnicornPreferLogicalOperatorOverTernary( diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index 3b0cea507122d..a89072251ff2b 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -531,6 +531,7 @@ pub(crate) mod unicorn { pub mod prefer_dom_node_text_content; pub mod prefer_event_target; pub mod prefer_global_this; + pub mod prefer_import_meta_properties; pub mod prefer_includes; pub mod prefer_keyboard_event_key; pub mod prefer_logical_operator_over_ternary; diff --git a/crates/oxc_linter/src/rules/unicorn/prefer_import_meta_properties.rs b/crates/oxc_linter/src/rules/unicorn/prefer_import_meta_properties.rs new file mode 100644 index 0000000000000..8b992e54b11f1 --- /dev/null +++ b/crates/oxc_linter/src/rules/unicorn/prefer_import_meta_properties.rs @@ -0,0 +1,594 @@ +use oxc_ast::{ + AstKind, + ast::{ + Argument, BindingPattern, CallExpression, Expression, MetaProperty, NewExpression, + VariableDeclarator, + }, +}; +use oxc_diagnostics::OxcDiagnostic; +use oxc_macros::declare_oxc_lint; +use oxc_span::{GetSpan, Span}; +use oxc_syntax::node::NodeId; +use oxc_syntax::symbol::SymbolId; +use rustc_hash::FxHashSet; + +use crate::{context::LintContext, rule::Rule}; + +const PATH_MODULES: [&str; 2] = ["path", "node:path"]; +const URL_MODULES: [&str; 2] = ["url", "node:url"]; + +#[derive(Clone, Copy, PartialEq, Eq)] +enum CheckKind { + Module, + Property, +} + +#[derive(Clone, Copy)] +enum ProblemKind { + Dirname, + Filename, +} + +impl ProblemKind { + fn message(self) -> &'static str { + match self { + Self::Dirname => "Do not construct dirname.", + Self::Filename => "Do not construct filename using `fileURLToPath()`.", + } + } + + fn property(self) -> &'static str { + match self { + Self::Dirname => "dirname", + Self::Filename => "filename", + } + } +} + +fn prefer_import_meta_diagnostic(span: Span, kind: ProblemKind) -> OxcDiagnostic { + OxcDiagnostic::warn(kind.message()) + .with_help(format!("Replace this expression with `import.meta.{}`.", kind.property())) + .with_label(span) +} + +fn report_problem(ctx: &LintContext<'_>, span: Span, kind: ProblemKind) { + ctx.diagnostic_with_fix(prefer_import_meta_diagnostic(span, kind), |fixer| { + fixer.replace(span, format!("import.meta.{}", kind.property())) + }); +} + +#[derive(Debug, Default, Clone)] +pub struct PreferImportMetaProperties; + +declare_oxc_lint!( + /// ### What it does + /// + /// Prefer `import.meta.{dirname,filename}` over legacy + /// techniques for getting file paths. + /// + /// ### Why is this bad? + /// + /// Starting with Node.js 20.11, `import.meta.dirname` and `import.meta.filename` + /// have been introduced in ES modules. + /// `import.meta.filename` is equivalent to `url.fileURLToPath(import.meta.url)`. + /// `import.meta.dirname` is equivalent to `path.dirname(import.meta.filename)`. + /// This rule replaces legacy patterns with `import.meta.dirname` and `import.meta.filename`. + /// + /// ### Examples + /// + /// Examples of **incorrect** code for this rule: + /// ```js + /// import path from "node:path" + /// import { fileURLToPath } from "url"; + /// + /// const filename = fileURLToPath(import.meta.url); + /// const dirname = path.dirname(fileURLToPath(import.meta.url)); + /// const dirname = path.dirname(import.meta.filename) + /// const dirname = fileURLToPath(new URL('.', import.meta.url)) + /// ``` + /// + /// Examples of **correct** code for this rule: + /// ```js + /// const filename = import.meta.filename; + /// const dirname = import.meta.dirname; + /// ``` + PreferImportMetaProperties, + unicorn, + pedantic, + fix, +); + +impl Rule for PreferImportMetaProperties { + fn run<'a>(&self, node: &oxc_semantic::AstNode<'a>, ctx: &LintContext<'a>) { + let AstKind::MetaProperty(meta_property) = node.kind() else { return }; + if !is_import_meta(meta_property) { + return; + } + + let member_expression_id = ctx.nodes().parent_id(meta_property.node_id()); + let AstKind::StaticMemberExpression(member_expression) = + ctx.nodes().kind(member_expression_id) + else { + return; + }; + if member_expression.optional { + return; + } + + match member_expression.property.name.as_str() { + "url" => { + let parent_id = ctx.nodes().parent_id(member_expression_id); + let member_expression_span = ctx.nodes().kind(member_expression_id).span(); + + if let AstKind::CallExpression(parent) = ctx.nodes().kind(parent_id) + && is_url_file_url_to_path_call(parent, ctx) + && has_argument_expression(&parent.arguments, 0, member_expression_span) + { + iterate_problems_from_filename(parent.node_id(), true, ctx); + return; + } + + if let AstKind::NewExpression(new_url) = ctx.nodes().kind(parent_id) + && is_url_constructor(new_url, ctx) + { + let url_parent_id = ctx.nodes().parent_id(new_url.node_id()); + let AstKind::CallExpression(url_parent) = ctx.nodes().kind(url_parent_id) + else { + return; + }; + + if !is_url_file_url_to_path_call(url_parent, ctx) + || !has_argument_expression(&url_parent.arguments, 0, new_url.span) + { + return; + } + + if new_url.arguments.len() == 1 + && has_argument_expression(&new_url.arguments, 0, member_expression_span) + { + iterate_problems_from_filename(url_parent.node_id(), true, ctx); + return; + } + + if new_url.arguments.len() == 2 + && is_parent_literal(&new_url.arguments[0]) + && has_argument_expression(&new_url.arguments, 1, member_expression_span) + { + report_problem(ctx, url_parent.span, ProblemKind::Dirname); + } + } + } + "filename" => iterate_problems_from_filename(member_expression.node_id(), false, ctx), + _ => {} + } + } +} + +/// Returns `true` if `call` is `process.getBuiltinModule("one_of_modules")`. +fn is_process_get_builtin_module_call(call: &CallExpression<'_>, modules: &[&str]) -> bool { + if call.optional || call.arguments.len() != 1 || call.arguments[0].is_spread() { + return false; + } + let Expression::StaticMemberExpression(callee_member) = &call.callee else { return false }; + if callee_member.optional { + return false; + } + let Expression::Identifier(process_ident) = &callee_member.object else { return false }; + if process_ident.name != "process" || callee_member.property.name != "getBuiltinModule" { + return false; + } + matches!( + &call.arguments[0], + Argument::StringLiteral(lit) if modules.contains(&lit.value.as_str()) + ) +} + +fn is_import_meta(meta_property: &MetaProperty<'_>) -> bool { + meta_property.meta.name == "import" && meta_property.property.name == "meta" +} + +fn is_parent_literal(argument: &Argument<'_>) -> bool { + matches!( + argument, + Argument::StringLiteral(lit) if lit.value.as_str() == "." || lit.value.as_str() == "./" + ) +} + +fn has_argument_expression(arguments: &[Argument<'_>], index: usize, node_span: Span) -> bool { + arguments + .get(index) + .and_then(Argument::as_expression) + .is_some_and(|expr| expr.span() == node_span) +} + +fn is_url_constructor(new_expression: &NewExpression<'_>, ctx: &LintContext<'_>) -> bool { + if !(1..=2).contains(&new_expression.arguments.len()) { + return false; + } + + let Expression::Identifier(identifier) = &new_expression.callee else { return false }; + if identifier.name != "URL" { + return false; + } + + ctx.scoping().get_reference(identifier.reference_id()).symbol_id().is_none() +} + +fn is_url_file_url_to_path_call( + call_expression: &CallExpression<'_>, + ctx: &LintContext<'_>, +) -> bool { + is_node_builtin_module_function_call(call_expression, &URL_MODULES, "fileURLToPath", ctx) +} + +fn is_path_dirname_call(call_expression: &CallExpression<'_>, ctx: &LintContext<'_>) -> bool { + is_node_builtin_module_function_call(call_expression, &PATH_MODULES, "dirname", ctx) +} + +fn is_node_builtin_module_function_call( + node: &CallExpression<'_>, + modules: &[&str], + function_name: &str, + ctx: &LintContext<'_>, +) -> bool { + if node.optional || node.arguments.len() != 1 || node.arguments[0].is_spread() { + return false; + } + let mut visited = FxHashSet::default(); + check_expression(&node.callee, CheckKind::Property, modules, function_name, ctx, &mut visited) +} + +fn check_expression( + node: &Expression<'_>, + check_kind: CheckKind, + modules: &[&str], + function_name: &str, + ctx: &LintContext<'_>, + visited: &mut FxHashSet, +) -> bool { + match node { + Expression::StaticMemberExpression(member_expression) => { + if !matches!(check_kind, CheckKind::Property) + || member_expression.optional + || member_expression.property.name != function_name + { + return false; + } + + check_expression( + &member_expression.object, + CheckKind::Module, + modules, + function_name, + ctx, + visited, + ) + } + Expression::CallExpression(call_expression) => { + matches!(check_kind, CheckKind::Module) + && is_process_get_builtin_module_call(call_expression, modules) + } + Expression::Identifier(identifier) => { + let reference = ctx.scoping().get_reference(identifier.reference_id()); + let Some(symbol_id) = reference.symbol_id() else { return false }; + if !visited.insert(symbol_id) { + return false; + } + check_definition(symbol_id, check_kind, modules, function_name, ctx, visited) + } + _ => false, + } +} + +fn check_definition( + symbol_id: SymbolId, + check_kind: CheckKind, + modules: &[&str], + function_name: &str, + ctx: &LintContext<'_>, + visited: &mut FxHashSet, +) -> bool { + let declaration_id = ctx.scoping().symbol_declaration(symbol_id); + match ctx.nodes().kind(declaration_id) { + AstKind::ImportSpecifier(import_specifier) => { + import_declaration_source(declaration_id, ctx).is_some_and(|source| { + matches!(check_kind, CheckKind::Property) + && modules.contains(&source) + && import_specifier.imported.name() == function_name + }) + } + AstKind::ImportDefaultSpecifier(_) | AstKind::ImportNamespaceSpecifier(_) => { + matches!(check_kind, CheckKind::Module) + && import_declaration_source(declaration_id, ctx) + .is_some_and(|source| modules.contains(&source)) + } + AstKind::VariableDeclarator(variable_declarator) => check_variable_declarator( + variable_declarator, + symbol_id, + check_kind, + modules, + function_name, + ctx, + visited, + ), + _ => false, + } +} + +fn import_declaration_source<'a>( + declaration_id: NodeId, + ctx: &'a LintContext<'_>, +) -> Option<&'a str> { + let AstKind::ImportDeclaration(import_declaration) = ctx.nodes().parent_kind(declaration_id) + else { + return None; + }; + Some(import_declaration.source.value.as_str()) +} + +fn check_variable_declarator( + variable_declarator: &VariableDeclarator<'_>, + symbol_id: SymbolId, + check_kind: CheckKind, + modules: &[&str], + function_name: &str, + ctx: &LintContext<'_>, + visited: &mut FxHashSet, +) -> bool { + if !variable_declarator.kind.is_const() { + return false; + } + + let Some(init) = &variable_declarator.init else { return false }; + + match &variable_declarator.id { + BindingPattern::BindingIdentifier(binding_identifier) + if binding_identifier.symbol_id() == symbol_id => + { + check_expression(init, check_kind, modules, function_name, ctx, visited) + } + BindingPattern::ObjectPattern(object_pattern) => { + for property in &object_pattern.properties { + if property.computed { + continue; + } + let BindingPattern::BindingIdentifier(binding_identifier) = &property.value else { + continue; + }; + if binding_identifier.symbol_id() != symbol_id { + continue; + } + let Some(property_name) = property.key.static_name() else { return false }; + if !matches!(check_kind, CheckKind::Property) || property_name != function_name { + return false; + } + return check_expression( + init, + CheckKind::Module, + modules, + function_name, + ctx, + visited, + ); + } + false + } + _ => false, + } +} + +fn iterate_problems_from_filename( + node_id: NodeId, + report_filename_node: bool, + ctx: &LintContext<'_>, +) { + let parent_id = ctx.nodes().parent_id(node_id); + let node_span = ctx.nodes().kind(node_id).span(); + + if let AstKind::CallExpression(parent) = ctx.nodes().kind(parent_id) + && is_path_dirname_call(parent, ctx) + && has_argument_expression(&parent.arguments, 0, node_span) + { + report_problem(ctx, parent.span, ProblemKind::Dirname); + return; + } + + if report_filename_node { + report_problem(ctx, ctx.nodes().kind(node_id).span(), ProblemKind::Filename); + } + + let AstKind::VariableDeclarator(parent) = ctx.nodes().kind(parent_id) else { return }; + if !parent.kind.is_const() || parent.init.as_ref().is_none_or(|init| init.span() != node_span) { + return; + } + + let BindingPattern::BindingIdentifier(binding_identifier) = &parent.id else { return }; + for reference in + ctx.semantic().symbol_references(binding_identifier.symbol_id()).filter(|r| r.is_read()) + { + let reference_parent_id = ctx.nodes().parent_id(reference.node_id()); + let AstKind::CallExpression(parent) = ctx.nodes().kind(reference_parent_id) else { + continue; + }; + if is_path_dirname_call(parent, ctx) + && has_argument_expression( + &parent.arguments, + 0, + ctx.nodes().get_node(reference.node_id()).kind().span(), + ) + { + report_problem(ctx, parent.span, ProblemKind::Dirname); + } + } +} + +#[test] +fn test() { + use crate::tester::{ExpectFixTestCase, Tester}; + + let pass = vec![ + "const __dirname = import.meta.dirname;", + "const __filename = import.meta.filename;", + r#"import path from "path"; + const dirUrl = path.dirname(import.meta.url);"#, + "const url = import.meta.url;", + r#"const dirname = new URL(".", import.meta.url).pathname;"#, + "const filename = new URL(import.meta.url).pathname;", + "const filename = fileURLToPath(import.meta.url);", + "const dirname = path.dirname(import.meta.filename);", + r#"import path from "path"; + // It is the same as dirname on macOS but returns different results on Windows. + const notDirname = path.dirname(new URL(import.meta.url).pathname);"#, + "// path is not initialized + let path; + const dirname = path.dirname(import.meta.filename);", + r#"// path is unknown property + const { path } = process.getBuiltinModule("node:path"); + const dirname = path.dirname(import.meta.filename);"#, + r#"const { dirname } = process.getBuiltinModule("node:path"); + // dirname()() is unknown + const x = dirname(x)(import.meta.filename);"#, + "// path is unknown + const path = new X(); + const dirname = path.dirname(import.meta.filename);", + "// path is unknown + const path = path; + const dirname = path.dirname(import.meta.filename);", + r#"// path is unknown + const [path] = process.getBuiltinModule("node:path"); + const dirname = path.dirname(import.meta.filename);"#, + r#"import path from "path"; + const dirname = path?.dirname(import.meta.filename);"#, + r#"import path from "path"; + const dirname = path[dirname](import.meta.filename);"#, + r#"import path from "path"; + const dirname = path["dirname"](import.meta.filename);"#, + r#"import path from "path"; + const dirname = path.dirname?.(import.meta.filename);"#, + r#"const { [fileURLToPath]: fileURLToPath } = process.getBuiltinModule("node:url"); + const filename = fileURLToPath(import.meta.url);"#, + r#"const { ["fileURLToPath"]: fileURLToPath } = process.getBuiltinModule("node:url"); + const filename = fileURLToPath(import.meta.url);"#, + r#"import {fileURLToPath} from "node:url"; + class Foo { + constructor() { + const filename = fileURLToPath(new.target.url) + } + }"#, + r#"import {fileURLToPath} from "node:url"; + const filename = fileURLToPath(import.meta?.url)"#, + r#"import {fileURLToPath} from "node:url"; + const filename = fileURLToPath(import.meta['url'])"#, + // URL is shadowed by a local binding — should not flag + r#"import { fileURLToPath } from "url"; + const URL = class {}; + const filename = fileURLToPath(new URL(import.meta.url));"#, + // new URL with non-import.meta.url argument — should not flag + r#"import { fileURLToPath } from "url"; + const filename = fileURLToPath(new URL("https://example.com"));"#, + r#"import { fileURLToPath } from "url"; + const dirname = fileURLToPath(new URL(".", "https://example.com"));"#, + ]; + + let fail = vec![ + r#"import path from "path"; + import { fileURLToPath } from "url"; + const dirname = path.dirname(fileURLToPath(import.meta.url));"#, + r#"import path from "path"; + const dirname = path.dirname(import.meta.filename);"#, + r#"import { fileURLToPath } from "url"; + const dirname = fileURLToPath(new URL(".", import.meta.url));"#, + r#"import { fileURLToPath } from "url"; + const dirname = fileURLToPath(new URL("./", import.meta.url));"#, + r#"import { fileURLToPath } from "url"; + const filename = fileURLToPath(import.meta.url);"#, + r#"import { fileURLToPath } from "url"; + const filename = fileURLToPath(new URL(import.meta.url));"#, + r#"import path from "node:path"; + import { fileURLToPath } from "node:url"; + const dirname = path.dirname(fileURLToPath(import.meta.url));"#, + r#"import { fileURLToPath } from "node:url"; + const filename = fileURLToPath(import.meta.url);"#, + r#"import * as path from "node:path"; + import url from "node:url"; + const dirname = path.dirname(url.fileURLToPath(import.meta.url));"#, + r#"import url from "node:url"; + const filename = url.fileURLToPath(import.meta.url);"#, + r#"import path from "node:path"; + import { fileURLToPath } from "node:url"; + const __filename = fileURLToPath(import.meta.url); + const __dirname = path.dirname(__filename);"#, + r#"import path from "node:path"; + const __filename = import.meta.filename; + const __dirname = path.dirname(__filename);"#, + r#"const path = process.getBuiltinModule("node:path"); + const { fileURLToPath } = process.getBuiltinModule("node:url"); + const filename = fileURLToPath(import.meta.url); + const dirname = path.dirname(filename);"#, + r#"const { fileURLToPath: renamed } = process.getBuiltinModule("node:url"); + const filename = renamed(import.meta.url);"#, + r#"import { fileURLToPath as renamed } from "node:url"; + const filename = renamed(import.meta.url);"#, + r#"const path = process.getBuiltinModule("path"); + const { fileURLToPath } = process.getBuiltinModule("url"); + const filename = fileURLToPath(import.meta.url); + const dirname = path.dirname(filename);"#, + r#"const filename = process.getBuiltinModule("node:url").fileURLToPath(import.meta.url); + const dirname = process.getBuiltinModule("node:path").dirname(filename);"#, + // inline-chained without subsequent dirname — should emit filename diagnostic + r#"const filename = process.getBuiltinModule("node:url").fileURLToPath(import.meta.url);"#, + ]; + + let fix: Vec = vec![ + // filename: fileURLToPath(import.meta.url) → import.meta.filename + ( + r#"import { fileURLToPath } from "url"; + const filename = fileURLToPath(import.meta.url);"#, + r#"import { fileURLToPath } from "url"; + const filename = import.meta.filename;"#, + ) + .into(), + // dirname: path.dirname(fileURLToPath(import.meta.url)) → import.meta.dirname + ( + r#"import path from "path"; + import { fileURLToPath } from "url"; + const dirname = path.dirname(fileURLToPath(import.meta.url));"#, + r#"import path from "path"; + import { fileURLToPath } from "url"; + const dirname = import.meta.dirname;"#, + ) + .into(), + // dirname via new URL(".", …) + ( + r#"import { fileURLToPath } from "url"; + const dirname = fileURLToPath(new URL(".", import.meta.url));"#, + r#"import { fileURLToPath } from "url"; + const dirname = import.meta.dirname;"#, + ) + .into(), + // dirname via import.meta.filename passed to path.dirname + ( + r#"import path from "path"; + const dirname = path.dirname(import.meta.filename);"#, + r#"import path from "path"; + const dirname = import.meta.dirname;"#, + ) + .into(), + // dirname via multi-step: const __filename = …; path.dirname(__filename) + ( + r#"import path from "node:path"; + import { fileURLToPath } from "node:url"; + const __filename = fileURLToPath(import.meta.url); + const __dirname = path.dirname(__filename);"#, + r#"import path from "node:path"; + import { fileURLToPath } from "node:url"; + const __filename = import.meta.filename; + const __dirname = import.meta.dirname;"#, + ) + .into(), + ]; + + Tester::new(PreferImportMetaProperties::NAME, PreferImportMetaProperties::PLUGIN, pass, fail) + .expect_fix(fix) + .test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/unicorn_prefer_import_meta_properties.snap b/crates/oxc_linter/src/snapshots/unicorn_prefer_import_meta_properties.snap new file mode 100644 index 0000000000000..8e45fd27120b0 --- /dev/null +++ b/crates/oxc_linter/src/snapshots/unicorn_prefer_import_meta_properties.snap @@ -0,0 +1,181 @@ +--- +source: crates/oxc_linter/src/tester.rs +--- + + ⚠ eslint-plugin-unicorn(prefer-import-meta-properties): Do not construct dirname. + ╭─[prefer_import_meta_properties.tsx:3:29] + 2 │ import { fileURLToPath } from "url"; + 3 │ const dirname = path.dirname(fileURLToPath(import.meta.url)); + · ──────────────────────────────────────────── + ╰──── + help: Replace this expression with `import.meta.dirname`. + + ⚠ eslint-plugin-unicorn(prefer-import-meta-properties): Do not construct dirname. + ╭─[prefer_import_meta_properties.tsx:2:29] + 1 │ import path from "path"; + 2 │ const dirname = path.dirname(import.meta.filename); + · ────────────────────────────────── + ╰──── + help: Replace this expression with `import.meta.dirname`. + + ⚠ eslint-plugin-unicorn(prefer-import-meta-properties): Do not construct dirname. + ╭─[prefer_import_meta_properties.tsx:2:29] + 1 │ import { fileURLToPath } from "url"; + 2 │ const dirname = fileURLToPath(new URL(".", import.meta.url)); + · ──────────────────────────────────────────── + ╰──── + help: Replace this expression with `import.meta.dirname`. + + ⚠ eslint-plugin-unicorn(prefer-import-meta-properties): Do not construct dirname. + ╭─[prefer_import_meta_properties.tsx:2:29] + 1 │ import { fileURLToPath } from "url"; + 2 │ const dirname = fileURLToPath(new URL("./", import.meta.url)); + · ───────────────────────────────────────────── + ╰──── + help: Replace this expression with `import.meta.dirname`. + + ⚠ eslint-plugin-unicorn(prefer-import-meta-properties): Do not construct filename using `fileURLToPath()`. + ╭─[prefer_import_meta_properties.tsx:2:30] + 1 │ import { fileURLToPath } from "url"; + 2 │ const filename = fileURLToPath(import.meta.url); + · ────────────────────────────── + ╰──── + help: Replace this expression with `import.meta.filename`. + + ⚠ eslint-plugin-unicorn(prefer-import-meta-properties): Do not construct filename using `fileURLToPath()`. + ╭─[prefer_import_meta_properties.tsx:2:30] + 1 │ import { fileURLToPath } from "url"; + 2 │ const filename = fileURLToPath(new URL(import.meta.url)); + · ─────────────────────────────────────── + ╰──── + help: Replace this expression with `import.meta.filename`. + + ⚠ eslint-plugin-unicorn(prefer-import-meta-properties): Do not construct dirname. + ╭─[prefer_import_meta_properties.tsx:3:29] + 2 │ import { fileURLToPath } from "node:url"; + 3 │ const dirname = path.dirname(fileURLToPath(import.meta.url)); + · ──────────────────────────────────────────── + ╰──── + help: Replace this expression with `import.meta.dirname`. + + ⚠ eslint-plugin-unicorn(prefer-import-meta-properties): Do not construct filename using `fileURLToPath()`. + ╭─[prefer_import_meta_properties.tsx:2:30] + 1 │ import { fileURLToPath } from "node:url"; + 2 │ const filename = fileURLToPath(import.meta.url); + · ────────────────────────────── + ╰──── + help: Replace this expression with `import.meta.filename`. + + ⚠ eslint-plugin-unicorn(prefer-import-meta-properties): Do not construct dirname. + ╭─[prefer_import_meta_properties.tsx:3:29] + 2 │ import url from "node:url"; + 3 │ const dirname = path.dirname(url.fileURLToPath(import.meta.url)); + · ──────────────────────────────────────────────── + ╰──── + help: Replace this expression with `import.meta.dirname`. + + ⚠ eslint-plugin-unicorn(prefer-import-meta-properties): Do not construct filename using `fileURLToPath()`. + ╭─[prefer_import_meta_properties.tsx:2:30] + 1 │ import url from "node:url"; + 2 │ const filename = url.fileURLToPath(import.meta.url); + · ────────────────────────────────── + ╰──── + help: Replace this expression with `import.meta.filename`. + + ⚠ eslint-plugin-unicorn(prefer-import-meta-properties): Do not construct filename using `fileURLToPath()`. + ╭─[prefer_import_meta_properties.tsx:3:32] + 2 │ import { fileURLToPath } from "node:url"; + 3 │ const __filename = fileURLToPath(import.meta.url); + · ────────────────────────────── + 4 │ const __dirname = path.dirname(__filename); + ╰──── + help: Replace this expression with `import.meta.filename`. + + ⚠ eslint-plugin-unicorn(prefer-import-meta-properties): Do not construct dirname. + ╭─[prefer_import_meta_properties.tsx:4:31] + 3 │ const __filename = fileURLToPath(import.meta.url); + 4 │ const __dirname = path.dirname(__filename); + · ──────────────────────── + ╰──── + help: Replace this expression with `import.meta.dirname`. + + ⚠ eslint-plugin-unicorn(prefer-import-meta-properties): Do not construct dirname. + ╭─[prefer_import_meta_properties.tsx:3:31] + 2 │ const __filename = import.meta.filename; + 3 │ const __dirname = path.dirname(__filename); + · ──────────────────────── + ╰──── + help: Replace this expression with `import.meta.dirname`. + + ⚠ eslint-plugin-unicorn(prefer-import-meta-properties): Do not construct filename using `fileURLToPath()`. + ╭─[prefer_import_meta_properties.tsx:3:30] + 2 │ const { fileURLToPath } = process.getBuiltinModule("node:url"); + 3 │ const filename = fileURLToPath(import.meta.url); + · ────────────────────────────── + 4 │ const dirname = path.dirname(filename); + ╰──── + help: Replace this expression with `import.meta.filename`. + + ⚠ eslint-plugin-unicorn(prefer-import-meta-properties): Do not construct dirname. + ╭─[prefer_import_meta_properties.tsx:4:29] + 3 │ const filename = fileURLToPath(import.meta.url); + 4 │ const dirname = path.dirname(filename); + · ────────────────────── + ╰──── + help: Replace this expression with `import.meta.dirname`. + + ⚠ eslint-plugin-unicorn(prefer-import-meta-properties): Do not construct filename using `fileURLToPath()`. + ╭─[prefer_import_meta_properties.tsx:2:30] + 1 │ const { fileURLToPath: renamed } = process.getBuiltinModule("node:url"); + 2 │ const filename = renamed(import.meta.url); + · ──────────────────────── + ╰──── + help: Replace this expression with `import.meta.filename`. + + ⚠ eslint-plugin-unicorn(prefer-import-meta-properties): Do not construct filename using `fileURLToPath()`. + ╭─[prefer_import_meta_properties.tsx:2:30] + 1 │ import { fileURLToPath as renamed } from "node:url"; + 2 │ const filename = renamed(import.meta.url); + · ──────────────────────── + ╰──── + help: Replace this expression with `import.meta.filename`. + + ⚠ eslint-plugin-unicorn(prefer-import-meta-properties): Do not construct filename using `fileURLToPath()`. + ╭─[prefer_import_meta_properties.tsx:3:30] + 2 │ const { fileURLToPath } = process.getBuiltinModule("url"); + 3 │ const filename = fileURLToPath(import.meta.url); + · ────────────────────────────── + 4 │ const dirname = path.dirname(filename); + ╰──── + help: Replace this expression with `import.meta.filename`. + + ⚠ eslint-plugin-unicorn(prefer-import-meta-properties): Do not construct dirname. + ╭─[prefer_import_meta_properties.tsx:4:29] + 3 │ const filename = fileURLToPath(import.meta.url); + 4 │ const dirname = path.dirname(filename); + · ────────────────────── + ╰──── + help: Replace this expression with `import.meta.dirname`. + + ⚠ eslint-plugin-unicorn(prefer-import-meta-properties): Do not construct filename using `fileURLToPath()`. + ╭─[prefer_import_meta_properties.tsx:1:18] + 1 │ const filename = process.getBuiltinModule("node:url").fileURLToPath(import.meta.url); + · ─────────────────────────────────────────────────────────────────── + 2 │ const dirname = process.getBuiltinModule("node:path").dirname(filename); + ╰──── + help: Replace this expression with `import.meta.filename`. + + ⚠ eslint-plugin-unicorn(prefer-import-meta-properties): Do not construct dirname. + ╭─[prefer_import_meta_properties.tsx:2:29] + 1 │ const filename = process.getBuiltinModule("node:url").fileURLToPath(import.meta.url); + 2 │ const dirname = process.getBuiltinModule("node:path").dirname(filename); + · ─────────────────────────────────────────────────────── + ╰──── + help: Replace this expression with `import.meta.dirname`. + + ⚠ eslint-plugin-unicorn(prefer-import-meta-properties): Do not construct filename using `fileURLToPath()`. + ╭─[prefer_import_meta_properties.tsx:1:18] + 1 │ const filename = process.getBuiltinModule("node:url").fileURLToPath(import.meta.url); + · ─────────────────────────────────────────────────────────────────── + ╰──── + help: Replace this expression with `import.meta.filename`.