diff --git a/crates/oxc_linter/src/generated/rule_runner_impls.rs b/crates/oxc_linter/src/generated/rule_runner_impls.rs index 973140fc5040c..c4ac064ef9a2a 100644 --- a/crates/oxc_linter/src/generated/rule_runner_impls.rs +++ b/crates/oxc_linter/src/generated/rule_runner_impls.rs @@ -1957,6 +1957,17 @@ impl RuleRunner for crate::rules::typescript::unbound_method::UnboundMethod { const RUN_FUNCTIONS: RuleRunFunctionsImplemented = RuleRunFunctionsImplemented::Unknown; } +impl RuleRunner for crate::rules::typescript::unified_signatures::UnifiedSignatures { + const NODE_TYPES: Option<&AstTypesBitset> = Some(&AstTypesBitset::from_types(&[ + AstType::Class, + AstType::Program, + AstType::TSInterfaceDeclaration, + AstType::TSModuleBlock, + AstType::TSTypeLiteral, + ])); + const RUN_FUNCTIONS: RuleRunFunctionsImplemented = RuleRunFunctionsImplemented::Run; +} + impl RuleRunner for crate::rules::typescript::use_unknown_in_catch_callback_variable::UseUnknownInCatchCallbackVariable { const NODE_TYPES: Option<&AstTypesBitset> = None; const RUN_FUNCTIONS: RuleRunFunctionsImplemented = RuleRunFunctionsImplemented::Unknown; diff --git a/crates/oxc_linter/src/generated/rules_enum.rs b/crates/oxc_linter/src/generated/rules_enum.rs index 189629a4cdb03..bc97be087fa84 100644 --- a/crates/oxc_linter/src/generated/rules_enum.rs +++ b/crates/oxc_linter/src/generated/rules_enum.rs @@ -528,6 +528,7 @@ pub use crate::rules::typescript::strict_boolean_expressions::StrictBooleanExpre pub use crate::rules::typescript::switch_exhaustiveness_check::SwitchExhaustivenessCheck as TypescriptSwitchExhaustivenessCheck; pub use crate::rules::typescript::triple_slash_reference::TripleSlashReference as TypescriptTripleSlashReference; pub use crate::rules::typescript::unbound_method::UnboundMethod as TypescriptUnboundMethod; +pub use crate::rules::typescript::unified_signatures::UnifiedSignatures as TypescriptUnifiedSignatures; pub use crate::rules::typescript::use_unknown_in_catch_callback_variable::UseUnknownInCatchCallbackVariable as TypescriptUseUnknownInCatchCallbackVariable; pub use crate::rules::unicorn::catch_error_name::CatchErrorName as UnicornCatchErrorName; pub use crate::rules::unicorn::consistent_assert::ConsistentAssert as UnicornConsistentAssert; @@ -988,6 +989,7 @@ pub enum RuleEnum { TypescriptSwitchExhaustivenessCheck(TypescriptSwitchExhaustivenessCheck), TypescriptTripleSlashReference(TypescriptTripleSlashReference), TypescriptUnboundMethod(TypescriptUnboundMethod), + TypescriptUnifiedSignatures(TypescriptUnifiedSignatures), TypescriptUseUnknownInCatchCallbackVariable(TypescriptUseUnknownInCatchCallbackVariable), JestConsistentTestIt(JestConsistentTestIt), JestExpectExpect(JestExpectExpect), @@ -1705,8 +1707,9 @@ const TYPESCRIPT_SWITCH_EXHAUSTIVENESS_CHECK_ID: usize = const TYPESCRIPT_TRIPLE_SLASH_REFERENCE_ID: usize = TYPESCRIPT_SWITCH_EXHAUSTIVENESS_CHECK_ID + 1usize; const TYPESCRIPT_UNBOUND_METHOD_ID: usize = TYPESCRIPT_TRIPLE_SLASH_REFERENCE_ID + 1usize; +const TYPESCRIPT_UNIFIED_SIGNATURES_ID: usize = TYPESCRIPT_UNBOUND_METHOD_ID + 1usize; const TYPESCRIPT_USE_UNKNOWN_IN_CATCH_CALLBACK_VARIABLE_ID: usize = - TYPESCRIPT_UNBOUND_METHOD_ID + 1usize; + TYPESCRIPT_UNIFIED_SIGNATURES_ID + 1usize; const JEST_CONSISTENT_TEST_IT_ID: usize = TYPESCRIPT_USE_UNKNOWN_IN_CATCH_CALLBACK_VARIABLE_ID + 1usize; const JEST_EXPECT_EXPECT_ID: usize = JEST_CONSISTENT_TEST_IT_ID + 1usize; @@ -2480,6 +2483,7 @@ impl RuleEnum { } Self::TypescriptTripleSlashReference(_) => TYPESCRIPT_TRIPLE_SLASH_REFERENCE_ID, Self::TypescriptUnboundMethod(_) => TYPESCRIPT_UNBOUND_METHOD_ID, + Self::TypescriptUnifiedSignatures(_) => TYPESCRIPT_UNIFIED_SIGNATURES_ID, Self::TypescriptUseUnknownInCatchCallbackVariable(_) => { TYPESCRIPT_USE_UNKNOWN_IN_CATCH_CALLBACK_VARIABLE_ID } @@ -3253,6 +3257,7 @@ impl RuleEnum { } Self::TypescriptTripleSlashReference(_) => TypescriptTripleSlashReference::NAME, Self::TypescriptUnboundMethod(_) => TypescriptUnboundMethod::NAME, + Self::TypescriptUnifiedSignatures(_) => TypescriptUnifiedSignatures::NAME, Self::TypescriptUseUnknownInCatchCallbackVariable(_) => { TypescriptUseUnknownInCatchCallbackVariable::NAME } @@ -4040,6 +4045,7 @@ impl RuleEnum { } Self::TypescriptTripleSlashReference(_) => TypescriptTripleSlashReference::CATEGORY, Self::TypescriptUnboundMethod(_) => TypescriptUnboundMethod::CATEGORY, + Self::TypescriptUnifiedSignatures(_) => TypescriptUnifiedSignatures::CATEGORY, Self::TypescriptUseUnknownInCatchCallbackVariable(_) => { TypescriptUseUnknownInCatchCallbackVariable::CATEGORY } @@ -4830,6 +4836,7 @@ impl RuleEnum { } Self::TypescriptTripleSlashReference(_) => TypescriptTripleSlashReference::FIX, Self::TypescriptUnboundMethod(_) => TypescriptUnboundMethod::FIX, + Self::TypescriptUnifiedSignatures(_) => TypescriptUnifiedSignatures::FIX, Self::TypescriptUseUnknownInCatchCallbackVariable(_) => { TypescriptUseUnknownInCatchCallbackVariable::FIX } @@ -5678,6 +5685,7 @@ impl RuleEnum { TypescriptTripleSlashReference::documentation() } Self::TypescriptUnboundMethod(_) => TypescriptUnboundMethod::documentation(), + Self::TypescriptUnifiedSignatures(_) => TypescriptUnifiedSignatures::documentation(), Self::TypescriptUseUnknownInCatchCallbackVariable(_) => { TypescriptUseUnknownInCatchCallbackVariable::documentation() } @@ -7041,6 +7049,10 @@ impl RuleEnum { } Self::TypescriptUnboundMethod(_) => TypescriptUnboundMethod::config_schema(generator) .or_else(|| TypescriptUnboundMethod::schema(generator)), + Self::TypescriptUnifiedSignatures(_) => { + TypescriptUnifiedSignatures::config_schema(generator) + .or_else(|| TypescriptUnifiedSignatures::schema(generator)) + } Self::TypescriptUseUnknownInCatchCallbackVariable(_) => { TypescriptUseUnknownInCatchCallbackVariable::config_schema(generator) .or_else(|| TypescriptUseUnknownInCatchCallbackVariable::schema(generator)) @@ -8414,6 +8426,7 @@ impl RuleEnum { Self::TypescriptSwitchExhaustivenessCheck(_) => "typescript", Self::TypescriptTripleSlashReference(_) => "typescript", Self::TypescriptUnboundMethod(_) => "typescript", + Self::TypescriptUnifiedSignatures(_) => "typescript", Self::TypescriptUseUnknownInCatchCallbackVariable(_) => "typescript", Self::JestConsistentTestIt(_) => "jest", Self::JestExpectExpect(_) => "jest", @@ -9754,6 +9767,9 @@ impl RuleEnum { Self::TypescriptUnboundMethod(_) => Ok(Self::TypescriptUnboundMethod( TypescriptUnboundMethod::from_configuration(value)?, )), + Self::TypescriptUnifiedSignatures(_) => Ok(Self::TypescriptUnifiedSignatures( + TypescriptUnifiedSignatures::from_configuration(value)?, + )), Self::TypescriptUseUnknownInCatchCallbackVariable(_) => { Ok(Self::TypescriptUseUnknownInCatchCallbackVariable( TypescriptUseUnknownInCatchCallbackVariable::from_configuration(value)?, @@ -11259,6 +11275,7 @@ impl RuleEnum { Self::TypescriptSwitchExhaustivenessCheck(rule) => rule.to_configuration(), Self::TypescriptTripleSlashReference(rule) => rule.to_configuration(), Self::TypescriptUnboundMethod(rule) => rule.to_configuration(), + Self::TypescriptUnifiedSignatures(rule) => rule.to_configuration(), Self::TypescriptUseUnknownInCatchCallbackVariable(rule) => rule.to_configuration(), Self::JestConsistentTestIt(rule) => rule.to_configuration(), Self::JestExpectExpect(rule) => rule.to_configuration(), @@ -11940,6 +11957,7 @@ impl RuleEnum { Self::TypescriptSwitchExhaustivenessCheck(rule) => rule.run(node, ctx), Self::TypescriptTripleSlashReference(rule) => rule.run(node, ctx), Self::TypescriptUnboundMethod(rule) => rule.run(node, ctx), + Self::TypescriptUnifiedSignatures(rule) => rule.run(node, ctx), Self::TypescriptUseUnknownInCatchCallbackVariable(rule) => rule.run(node, ctx), Self::JestConsistentTestIt(rule) => rule.run(node, ctx), Self::JestExpectExpect(rule) => rule.run(node, ctx), @@ -12619,6 +12637,7 @@ impl RuleEnum { Self::TypescriptSwitchExhaustivenessCheck(rule) => rule.run_once(ctx), Self::TypescriptTripleSlashReference(rule) => rule.run_once(ctx), Self::TypescriptUnboundMethod(rule) => rule.run_once(ctx), + Self::TypescriptUnifiedSignatures(rule) => rule.run_once(ctx), Self::TypescriptUseUnknownInCatchCallbackVariable(rule) => rule.run_once(ctx), Self::JestConsistentTestIt(rule) => rule.run_once(ctx), Self::JestExpectExpect(rule) => rule.run_once(ctx), @@ -13354,6 +13373,7 @@ impl RuleEnum { } Self::TypescriptTripleSlashReference(rule) => rule.run_on_jest_node(jest_node, ctx), Self::TypescriptUnboundMethod(rule) => rule.run_on_jest_node(jest_node, ctx), + Self::TypescriptUnifiedSignatures(rule) => rule.run_on_jest_node(jest_node, ctx), Self::TypescriptUseUnknownInCatchCallbackVariable(rule) => { rule.run_on_jest_node(jest_node, ctx) } @@ -14067,6 +14087,7 @@ impl RuleEnum { Self::TypescriptSwitchExhaustivenessCheck(rule) => rule.should_run(ctx), Self::TypescriptTripleSlashReference(rule) => rule.should_run(ctx), Self::TypescriptUnboundMethod(rule) => rule.should_run(ctx), + Self::TypescriptUnifiedSignatures(rule) => rule.should_run(ctx), Self::TypescriptUseUnknownInCatchCallbackVariable(rule) => rule.should_run(ctx), Self::JestConsistentTestIt(rule) => rule.should_run(ctx), Self::JestExpectExpect(rule) => rule.should_run(ctx), @@ -14880,6 +14901,7 @@ impl RuleEnum { TypescriptTripleSlashReference::IS_TSGOLINT_RULE } Self::TypescriptUnboundMethod(_) => TypescriptUnboundMethod::IS_TSGOLINT_RULE, + Self::TypescriptUnifiedSignatures(_) => TypescriptUnifiedSignatures::IS_TSGOLINT_RULE, Self::TypescriptUseUnknownInCatchCallbackVariable(_) => { TypescriptUseUnknownInCatchCallbackVariable::IS_TSGOLINT_RULE } @@ -15788,6 +15810,7 @@ impl RuleEnum { } Self::TypescriptTripleSlashReference(_) => TypescriptTripleSlashReference::HAS_CONFIG, Self::TypescriptUnboundMethod(_) => TypescriptUnboundMethod::HAS_CONFIG, + Self::TypescriptUnifiedSignatures(_) => TypescriptUnifiedSignatures::HAS_CONFIG, Self::TypescriptUseUnknownInCatchCallbackVariable(_) => { TypescriptUseUnknownInCatchCallbackVariable::HAS_CONFIG } @@ -16537,6 +16560,7 @@ impl RuleEnum { Self::TypescriptSwitchExhaustivenessCheck(rule) => rule.types_info(), Self::TypescriptTripleSlashReference(rule) => rule.types_info(), Self::TypescriptUnboundMethod(rule) => rule.types_info(), + Self::TypescriptUnifiedSignatures(rule) => rule.types_info(), Self::TypescriptUseUnknownInCatchCallbackVariable(rule) => rule.types_info(), Self::JestConsistentTestIt(rule) => rule.types_info(), Self::JestExpectExpect(rule) => rule.types_info(), @@ -17216,6 +17240,7 @@ impl RuleEnum { Self::TypescriptSwitchExhaustivenessCheck(rule) => rule.run_info(), Self::TypescriptTripleSlashReference(rule) => rule.run_info(), Self::TypescriptUnboundMethod(rule) => rule.run_info(), + Self::TypescriptUnifiedSignatures(rule) => rule.run_info(), Self::TypescriptUseUnknownInCatchCallbackVariable(rule) => rule.run_info(), Self::JestConsistentTestIt(rule) => rule.run_info(), Self::JestExpectExpect(rule) => rule.run_info(), @@ -17969,6 +17994,7 @@ pub static RULES: std::sync::LazyLock> = std::sync::LazyLock::new( ), RuleEnum::TypescriptTripleSlashReference(TypescriptTripleSlashReference::default()), RuleEnum::TypescriptUnboundMethod(TypescriptUnboundMethod::default()), + RuleEnum::TypescriptUnifiedSignatures(TypescriptUnifiedSignatures::default()), RuleEnum::TypescriptUseUnknownInCatchCallbackVariable( TypescriptUseUnknownInCatchCallbackVariable::default(), ), diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index 7b0c99660608b..f898dda3db28e 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -305,6 +305,7 @@ pub(crate) mod typescript { pub mod switch_exhaustiveness_check; pub mod triple_slash_reference; pub mod unbound_method; + pub mod unified_signatures; pub mod use_unknown_in_catch_callback_variable; } diff --git a/crates/oxc_linter/src/rules/typescript/unified_signatures.rs b/crates/oxc_linter/src/rules/typescript/unified_signatures.rs new file mode 100644 index 0000000000000..5573aaf9a5ac7 --- /dev/null +++ b/crates/oxc_linter/src/rules/typescript/unified_signatures.rs @@ -0,0 +1,1692 @@ +use std::mem; + +use oxc_ast::{ + AstKind, + ast::{ + Class, ClassElement, ClassType, Declaration, ExportDefaultDeclarationKind, FormalParameter, + FormalParameterRest, Function, FunctionType, MethodDefinition, MethodDefinitionKind, + PropertyKey, Statement, TSCallSignatureDeclaration, TSConstructSignatureDeclaration, + TSMethodSignature, TSMethodSignatureKind, TSSignature, TSThisParameter, TSType, + TSTypeAnnotation, TSTypeParameter, TSTypeParameterDeclaration, + }, +}; +use oxc_diagnostics::{LabeledSpan, OxcDiagnostic}; +use oxc_macros::declare_oxc_lint; +use oxc_span::{GetSpan, Span}; +use rustc_hash::{FxHashMap, FxHashSet}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::{ + AstNode, + context::{ContextHost, LintContext}, + rule::{DefaultRuleConfig, Rule}, +}; + +fn unified_signatures_diagnostic, T: IntoIterator>( + message: String, + labels: T, +) -> OxcDiagnostic { + OxcDiagnostic::warn(message).with_labels(labels) +} + +#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema)] +#[serde(default, rename_all = "camelCase")] +pub struct UnifiedSignaturesOptions { + /// Whether to ignore parameter name differences when comparing signatures. If `false`, signatures + /// will not be considered unifiable if they have parameters in the same position with different + /// names, even if the parameter types are the same. + ignore_differently_named_parameters: bool, + /// Whether to ignore JSDoc differences when comparing signatures. If `false`, signatures will not + /// be considered unifiable if the closest leading block comments for the signatures are different, + /// even if the signatures themselves are identical. + #[serde(rename = "ignoreOverloadsWithDifferentJSDoc")] + ignore_overloads_with_different_jsdoc: bool, +} + +#[derive(Debug, Default, Clone, Deserialize, Serialize)] +pub struct UnifiedSignatures(UnifiedSignaturesOptions); + +declare_oxc_lint!( + /// ### What it does + /// + /// Disallow overload signatures that can be unified into one. + /// + /// ### Why is this bad? + /// + /// Duplicate overload signatures that only differ by a single type, or by an optional/rest + /// parameter, are harder to maintain and read than a single unified signature. + /// + /// ### Examples + /// + /// Examples of **incorrect** code for this rule: + /// ```ts + /// function f(a: number): void; + /// function f(a: string): void; + /// ``` + /// + /// Examples of **correct** code for this rule: + /// ```ts + /// function f(a: number | string): void; + /// ``` + UnifiedSignatures, + typescript, + style, + config = UnifiedSignaturesOptions, +); + +impl Rule for UnifiedSignatures { + fn from_configuration(value: serde_json::Value) -> Result { + serde_json::from_value::>(value).map(DefaultRuleConfig::into_inner) + } + + fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) { + match node.kind() { + AstKind::Program(program) => { + self.check_statement_scope(&program.body, None, ctx); + } + AstKind::TSModuleBlock(module_block) => { + self.check_statement_scope(&module_block.body, None, ctx); + } + AstKind::TSInterfaceDeclaration(interface_decl) => { + self.check_signature_scope( + &interface_decl.body.body, + interface_decl.type_parameters.as_deref(), + ctx, + ); + } + AstKind::TSTypeLiteral(type_literal) => { + self.check_signature_scope(&type_literal.members, None, ctx); + } + AstKind::Class(class) if class.r#type == ClassType::ClassDeclaration => { + self.check_class_scope(class, ctx); + } + _ => {} + } + } + + fn should_run(&self, ctx: &ContextHost) -> bool { + ctx.source_type().is_typescript() + } +} + +#[derive(Clone, Copy)] +struct OverloadCandidate<'a> { + signature: SignatureDefinition<'a>, + comment_target_start: u32, +} + +#[derive(Clone, Copy)] +enum SignatureDefinition<'a> { + Function(&'a Function<'a>), + MethodDefinition(&'a MethodDefinition<'a>), + TSMethodSignature(&'a TSMethodSignature<'a>), + TSCallSignatureDeclaration(&'a TSCallSignatureDeclaration<'a>), + TSConstructSignatureDeclaration(&'a TSConstructSignatureDeclaration<'a>), +} + +impl<'a> SignatureDefinition<'a> { + fn type_parameters(self) -> Option<&'a TSTypeParameterDeclaration<'a>> { + match self { + Self::Function(function) => function.type_parameters.as_deref(), + Self::MethodDefinition(method) => method.value.type_parameters.as_deref(), + Self::TSMethodSignature(method_signature) => { + method_signature.type_parameters.as_deref() + } + Self::TSCallSignatureDeclaration(call_signature) => { + call_signature.type_parameters.as_deref() + } + Self::TSConstructSignatureDeclaration(construct_signature) => { + construct_signature.type_parameters.as_deref() + } + } + } + + fn this_param(self) -> Option<&'a TSThisParameter<'a>> { + match self { + Self::Function(function) => function.this_param.as_deref(), + Self::MethodDefinition(method) => method.value.this_param.as_deref(), + Self::TSMethodSignature(method_signature) => method_signature.this_param.as_deref(), + Self::TSCallSignatureDeclaration(call_signature) => { + call_signature.this_param.as_deref() + } + Self::TSConstructSignatureDeclaration(_) => None, + } + } + + fn params(self) -> &'a [FormalParameter<'a>] { + match self { + Self::Function(function) => &function.params.items, + Self::MethodDefinition(method) => &method.value.params.items, + Self::TSMethodSignature(method_signature) => &method_signature.params.items, + Self::TSCallSignatureDeclaration(call_signature) => &call_signature.params.items, + Self::TSConstructSignatureDeclaration(construct_signature) => { + &construct_signature.params.items + } + } + } + + fn rest_param(self) -> Option<&'a FormalParameterRest<'a>> { + match self { + Self::Function(function) => function.params.rest.as_deref(), + Self::MethodDefinition(method) => method.value.params.rest.as_deref(), + Self::TSMethodSignature(method_signature) => method_signature.params.rest.as_deref(), + Self::TSCallSignatureDeclaration(call_signature) => { + call_signature.params.rest.as_deref() + } + Self::TSConstructSignatureDeclaration(construct_signature) => { + construct_signature.params.rest.as_deref() + } + } + } + + fn return_type(self) -> Option<&'a TSTypeAnnotation<'a>> { + match self { + Self::Function(function) => function.return_type.as_deref(), + Self::MethodDefinition(method) => method.value.return_type.as_deref(), + Self::TSMethodSignature(method_signature) => method_signature.return_type.as_deref(), + Self::TSCallSignatureDeclaration(call_signature) => { + call_signature.return_type.as_deref() + } + Self::TSConstructSignatureDeclaration(construct_signature) => { + construct_signature.return_type.as_deref() + } + } + } +} + +#[derive(Clone, Copy)] +enum Parameter<'a> { + This(&'a TSThisParameter<'a>), + Formal(&'a FormalParameter<'a>), + Rest(&'a FormalParameterRest<'a>), +} + +#[derive(Clone, Copy)] +enum Unify<'a> { + SingleParameterDifference { p0: Parameter<'a>, p1: Parameter<'a> }, + ExtraParameter { extra_parameter: Parameter<'a>, anchor_parameter: Option> }, +} + +impl UnifiedSignatures { + fn check_class_scope<'a>(&self, class: &'a Class<'a>, ctx: &LintContext<'a>) { + let mut overloads: FxHashMap>> = FxHashMap::default(); + let source_text = ctx.source_text(); + + for element in &class.body.body { + let ClassElement::MethodDefinition(method) = element else { + continue; + }; + if method.value.body.is_some() || is_getter_or_setter_method(method.kind) { + continue; + } + + let key = get_overload_key(method.computed, method.r#static, &method.key, source_text); + overloads.entry(key).or_default().push(OverloadCandidate { + signature: SignatureDefinition::MethodDefinition(method), + comment_target_start: method.span.start, + }); + } + + self.check_overloads(&overloads, class.type_parameters.as_deref(), ctx); + } + + fn check_signature_scope<'a>( + &self, + signatures: &'a [TSSignature<'a>], + type_parameters: Option<&'a TSTypeParameterDeclaration<'a>>, + ctx: &LintContext<'a>, + ) { + let mut overloads: FxHashMap>> = FxHashMap::default(); + let source_text = ctx.source_text(); + + for signature in signatures { + match signature { + TSSignature::TSCallSignatureDeclaration(call_signature) => { + let key = format!("{}{}()", overload_key_bit(false), overload_key_bit(false)); + overloads.entry(key).or_default().push(OverloadCandidate { + signature: SignatureDefinition::TSCallSignatureDeclaration(call_signature), + comment_target_start: call_signature.span.start, + }); + } + TSSignature::TSConstructSignatureDeclaration(construct_signature) => { + let key = format!( + "{}{}constructor", + overload_key_bit(false), + overload_key_bit(false) + ); + overloads.entry(key).or_default().push(OverloadCandidate { + signature: SignatureDefinition::TSConstructSignatureDeclaration( + construct_signature, + ), + comment_target_start: construct_signature.span.start, + }); + } + TSSignature::TSMethodSignature(method_signature) + if method_signature.kind == TSMethodSignatureKind::Method => + { + let key = get_overload_key( + method_signature.computed, + false, + &method_signature.key, + source_text, + ); + overloads.entry(key).or_default().push(OverloadCandidate { + signature: SignatureDefinition::TSMethodSignature(method_signature), + comment_target_start: method_signature.span.start, + }); + } + _ => {} + } + } + + self.check_overloads(&overloads, type_parameters, ctx); + } + + fn check_statement_scope<'a>( + &self, + statements: &'a [Statement<'a>], + type_parameters: Option<&'a TSTypeParameterDeclaration<'a>>, + ctx: &LintContext<'a>, + ) { + let mut overloads: FxHashMap>> = FxHashMap::default(); + + for statement in statements { + let Some((key, candidate)) = get_statement_overload(statement) else { + continue; + }; + overloads.entry(key).or_default().push(candidate); + } + + self.check_overloads(&overloads, type_parameters, ctx); + } + + fn check_overloads<'a>( + &self, + overloads: &FxHashMap>>, + type_parameters: Option<&'a TSTypeParameterDeclaration<'a>>, + ctx: &LintContext<'a>, + ) { + let outer_type_parameters: FxHashSet<&'a str> = type_parameters + .map(|type_parameters| { + type_parameters + .params + .iter() + .map(|type_parameter| type_parameter.name.name.as_str()) + .collect() + }) + .unwrap_or_default(); + + for signatures in overloads.values() { + if signatures.len() < 2 { + continue; + } + + for i in 0..signatures.len() { + for j in (i + 1)..signatures.len() { + let Some(unify) = compare_signatures( + &signatures[i], + &signatures[j], + &outer_type_parameters, + &self.0, + ctx, + ) else { + continue; + }; + report_failure(unify, signatures.len() == 2, ctx); + } + } + } + } +} + +fn get_statement_overload<'a>( + statement: &'a Statement<'a>, +) -> Option<(String, OverloadCandidate<'a>)> { + if let Some(declaration) = statement.as_declaration() { + let Declaration::FunctionDeclaration(function) = declaration else { + return None; + }; + if function.r#type != FunctionType::TSDeclareFunction { + return None; + } + let key = get_function_overload_key(function, None)?; + return Some(( + key, + OverloadCandidate { + signature: SignatureDefinition::Function(function), + comment_target_start: function.span.start, + }, + )); + } + + let module_declaration = statement.as_module_declaration()?; + match module_declaration { + oxc_ast::ast::ModuleDeclaration::ExportNamedDeclaration(export_named) => { + let Some(Declaration::FunctionDeclaration(function)) = &export_named.declaration else { + return None; + }; + if function.r#type != FunctionType::TSDeclareFunction { + return None; + } + + let key = get_function_overload_key(function, Some("ExportNamedDeclaration"))?; + Some(( + key, + OverloadCandidate { + signature: SignatureDefinition::Function(function), + comment_target_start: export_named.span.start, + }, + )) + } + oxc_ast::ast::ModuleDeclaration::ExportDefaultDeclaration(export_default) => { + let ExportDefaultDeclarationKind::FunctionDeclaration(function) = + &export_default.declaration + else { + return None; + }; + if function.r#type != FunctionType::TSDeclareFunction { + return None; + } + + let key = get_function_overload_key(function, Some("ExportDefaultDeclaration"))?; + Some(( + key, + OverloadCandidate { + signature: SignatureDefinition::Function(function), + comment_target_start: export_default.span.start, + }, + )) + } + _ => None, + } +} + +fn get_function_overload_key( + function: &Function<'_>, + exporting_node_kind: Option<&str>, +) -> Option { + function + .id + .as_ref() + .map(|id| id.name.to_string()) + .or_else(|| exporting_node_kind.map(ToString::to_string)) +} + +fn overload_key_bit(value: bool) -> char { + if value { '0' } else { '1' } +} + +fn get_overload_key( + computed: bool, + r#static: bool, + key: &PropertyKey<'_>, + source_text: &str, +) -> String { + let info = match key { + PropertyKey::PrivateIdentifier(identifier) => { + format!("private_identifier_{}", identifier.name) + } + PropertyKey::StaticIdentifier(identifier) => format!("identifier_{}", identifier.name), + _ => key.span().source_text(source_text).to_string(), + }; + + format!("{}{}{}", overload_key_bit(computed), overload_key_bit(r#static), info) +} + +fn is_getter_or_setter_method(kind: MethodDefinitionKind) -> bool { + matches!(kind, MethodDefinitionKind::Get | MethodDefinitionKind::Set) +} + +fn compare_signatures<'a>( + first: &OverloadCandidate<'a>, + second: &OverloadCandidate<'a>, + outer_type_parameters: &FxHashSet<&'a str>, + options: &UnifiedSignaturesOptions, + ctx: &LintContext<'a>, +) -> Option> { + if !signatures_can_be_unified(first, second, outer_type_parameters, options, ctx) { + return None; + } + + let first_parameters = signature_parameters(first.signature); + let second_parameters = signature_parameters(second.signature); + + if first_parameters.len() == second_parameters.len() { + return signatures_differ_by_single_parameter(&first_parameters, &second_parameters, ctx); + } + + signatures_differ_by_optional_or_rest_parameter(&first_parameters, &second_parameters, ctx) +} + +fn signatures_can_be_unified<'a>( + first: &OverloadCandidate<'a>, + second: &OverloadCandidate<'a>, + outer_type_parameters: &FxHashSet<&'a str>, + options: &UnifiedSignaturesOptions, + ctx: &LintContext<'a>, +) -> bool { + let source_text = ctx.source_text(); + let first_signature = first.signature; + let second_signature = second.signature; + + let first_params = signature_parameters(first_signature); + let second_params = signature_parameters(second_signature); + + if options.ignore_differently_named_parameters { + let common_params_length = first_params.len().min(second_params.len()); + for i in 0..common_params_length { + if parameters_have_same_kind(first_params[i], second_params[i]) + && get_static_parameter_name(first_params[i]) + != get_static_parameter_name(second_params[i]) + { + return false; + } + } + } + + if options.ignore_overloads_with_different_jsdoc + && get_block_comment_for_node(first.comment_target_start, ctx) + != get_block_comment_for_node(second.comment_target_start, ctx) + { + return false; + } + + types_are_equal(first_signature.return_type(), second_signature.return_type(), source_text) + && type_parameter_declarations_are_equal( + first_signature.type_parameters(), + second_signature.type_parameters(), + ) + && signature_uses_outer_type_parameter(first_signature, outer_type_parameters) + == signature_uses_outer_type_parameter(second_signature, outer_type_parameters) +} + +fn get_block_comment_for_node<'a>(node_start: u32, ctx: &LintContext<'a>) -> Option<&'a str> { + ctx.comments() + .iter() + .rev() + .find(|comment| { + comment.attached_to == node_start && comment.is_leading() && comment.is_block() + }) + .map(|comment| comment.content_span().source_text(ctx.source_text())) +} + +fn type_parameter_declarations_are_equal( + first: Option<&TSTypeParameterDeclaration<'_>>, + second: Option<&TSTypeParameterDeclaration<'_>>, +) -> bool { + match (first, second) { + (None, None) => true, + (Some(first), Some(second)) => { + first.params.len() == second.params.len() + && first + .params + .iter() + .zip(second.params.iter()) + .all(|(first, second)| type_parameters_are_equal(first, second)) + } + _ => false, + } +} + +fn type_parameters_are_equal(first: &TSTypeParameter<'_>, second: &TSTypeParameter<'_>) -> bool { + first.name.name == second.name.name + && constraints_are_equal(first.constraint.as_ref(), second.constraint.as_ref()) +} + +fn constraints_are_equal(first: Option<&TSType<'_>>, second: Option<&TSType<'_>>) -> bool { + match (first, second) { + (None, None) => true, + (Some(first), Some(second)) => mem::discriminant(first) == mem::discriminant(second), + _ => false, + } +} + +fn signature_uses_outer_type_parameter( + signature: SignatureDefinition<'_>, + outer_type_parameters: &FxHashSet<&str>, +) -> bool { + signature_parameters(signature).into_iter().any(|parameter| { + parameter_type_annotation(parameter).is_some_and(|type_annotation| { + type_contains_outer_type_parameter( + &type_annotation.type_annotation, + outer_type_parameters, + ) + }) + }) +} + +fn type_contains_outer_type_parameter( + ty: &TSType<'_>, + outer_type_parameters: &FxHashSet<&str>, +) -> bool { + match ty { + TSType::TSTypeReference(type_reference) => type_reference + .type_name + .get_identifier_reference() + .is_some_and(|identifier_reference| { + outer_type_parameters.contains(identifier_reference.name.as_str()) + }), + TSType::TSArrayType(array) => { + type_contains_outer_type_parameter(&array.element_type, outer_type_parameters) + } + _ => false, + } +} + +fn signature_parameters(signature: SignatureDefinition<'_>) -> Vec> { + let mut parameters = Vec::with_capacity( + signature.params().len() + usize::from(signature.rest_param().is_some()) + 1, + ); + + if let Some(this_param) = signature.this_param() { + parameters.push(Parameter::This(this_param)); + } + + parameters.extend(signature.params().iter().map(Parameter::Formal)); + + if let Some(rest_param) = signature.rest_param() { + parameters.push(Parameter::Rest(rest_param)); + } + + parameters +} + +fn signatures_differ_by_single_parameter<'a>( + first: &[Parameter<'a>], + second: &[Parameter<'a>], + ctx: &LintContext<'a>, +) -> Option> { + let first_param = first.first().copied(); + let second_param = second.first().copied(); + + if first_param.is_some_and(is_this_void_param) || second_param.is_some_and(is_this_void_param) { + return None; + } + + let source_text = ctx.source_text(); + let index = get_index_of_first_difference(first, second, |first, second| { + parameters_are_equal(*first, *second, source_text) + })?; + + if !first[index + 1..] + .iter() + .zip(second[index + 1..].iter()) + .all(|(first, second)| parameters_are_equal(*first, *second, source_text)) + { + return None; + } + + let first = first[index]; + let second = second[index]; + + if parameters_have_equal_sigils(first, second) && !matches!(first, Parameter::Rest(_)) { + return Some(Unify::SingleParameterDifference { p0: first, p1: second }); + } + + None +} + +fn signatures_differ_by_optional_or_rest_parameter<'a>( + first_parameters: &[Parameter<'a>], + second_parameters: &[Parameter<'a>], + ctx: &LintContext<'a>, +) -> Option> { + let (longer, shorter) = if first_parameters.len() >= second_parameters.len() { + (first_parameters, second_parameters) + } else { + (second_parameters, first_parameters) + }; + + let first_param = first_parameters.first().copied(); + let second_param = second_parameters.first().copied(); + + if first_param.is_some_and(is_this_param) != second_param.is_some_and(is_this_param) { + return None; + } + if first_param.is_some_and(is_this_void_param) || second_param.is_some_and(is_this_void_param) { + return None; + } + + let min_length = first_parameters.len().min(second_parameters.len()); + + for parameter in longer.iter().skip(min_length + 1) { + if !parameter_may_be_missing(*parameter) { + return None; + } + } + + for i in 0..min_length { + if !types_are_equal( + parameter_type_annotation(first_parameters[i]), + parameter_type_annotation(second_parameters[i]), + ctx.source_text(), + ) { + return None; + } + } + + if min_length > 0 && matches!(shorter[min_length - 1], Parameter::Rest(_)) { + return None; + } + + Some(Unify::ExtraParameter { + extra_parameter: *longer.last().unwrap(), + anchor_parameter: shorter.last().copied(), + }) +} + +fn report_failure(unify: Unify<'_>, only_two_overloads: bool, ctx: &LintContext<'_>) { + let failure_string_start = if only_two_overloads { + "These overloads can be combined into one signature" + } else { + "This overload can be combined with another overload into one signature" + }; + + match unify { + Unify::SingleParameterDifference { p0, p1 } => { + let source_text = ctx.source_text(); + let type1 = parameter_type_annotation(p0).map_or("unknown", |type_annotation| { + type_annotation.type_annotation.span().source_text(source_text) + }); + let type2 = parameter_type_annotation(p1).map_or("unknown", |type_annotation| { + type_annotation.type_annotation.span().source_text(source_text) + }); + + ctx.diagnostic(unified_signatures_diagnostic( + format!("{failure_string_start} taking `{type1} | {type2}`."), + [ + parameter_span(p1).primary_label("this parameter can be unified"), + parameter_span(p0).label("with this overload parameter"), + ], + )); + } + Unify::ExtraParameter { extra_parameter, anchor_parameter } => { + let message = if matches!(extra_parameter, Parameter::Rest(_)) { + format!("{failure_string_start} with a rest parameter.") + } else { + format!("{failure_string_start} with an optional parameter.") + }; + + let mut labels = vec![ + parameter_span(extra_parameter) + .primary_label("this parameter only appears in one overload"), + ]; + if let Some(anchor_parameter) = anchor_parameter { + labels.push(parameter_span(anchor_parameter).label("matching overload ends here")); + } + + ctx.diagnostic(unified_signatures_diagnostic(message, labels)); + } + } +} + +fn parameter_type_annotation(parameter: Parameter<'_>) -> Option<&TSTypeAnnotation<'_>> { + match parameter { + Parameter::This(this_param) => this_param.type_annotation.as_deref(), + Parameter::Formal(parameter) => parameter.type_annotation.as_deref(), + Parameter::Rest(parameter) => parameter.type_annotation.as_deref(), + } +} + +fn parameter_may_be_missing(parameter: Parameter<'_>) -> bool { + matches!(parameter, Parameter::Rest(_)) || parameter_is_optional(parameter) +} + +fn parameter_is_optional(parameter: Parameter<'_>) -> bool { + match parameter { + Parameter::Formal(parameter) => parameter.optional, + Parameter::This(_) | Parameter::Rest(_) => false, + } +} + +fn is_this_param(parameter: Parameter<'_>) -> bool { + matches!(parameter, Parameter::This(_)) +} + +fn is_this_void_param(parameter: Parameter<'_>) -> bool { + matches!( + parameter_type_annotation(parameter), + Some(type_annotation) + if matches!(&type_annotation.type_annotation, TSType::TSVoidKeyword(_)) + ) +} + +fn parameter_span(parameter: Parameter<'_>) -> Span { + match parameter { + Parameter::This(this_param) => this_param.span, + Parameter::Formal(parameter) => parameter.span, + Parameter::Rest(parameter) => parameter.span, + } +} + +fn get_static_parameter_name(parameter: Parameter<'_>) -> Option<&str> { + match parameter { + Parameter::This(_) => Some("this"), + Parameter::Formal(parameter) => { + parameter.pattern.get_identifier_name().map(|name| name.as_str()) + } + Parameter::Rest(parameter) => { + parameter.rest.argument.get_identifier_name().map(|name| name.as_str()) + } + } +} + +fn parameters_have_same_kind(first: Parameter<'_>, second: Parameter<'_>) -> bool { + matches!( + (first, second), + (Parameter::This(_), Parameter::This(_)) + | (Parameter::Formal(_), Parameter::Formal(_)) + | (Parameter::Rest(_), Parameter::Rest(_)) + ) +} + +fn parameters_have_equal_sigils(first: Parameter<'_>, second: Parameter<'_>) -> bool { + matches!(first, Parameter::Rest(_)) == matches!(second, Parameter::Rest(_)) + && parameter_is_optional(first) == parameter_is_optional(second) +} + +fn parameters_are_equal(first: Parameter<'_>, second: Parameter<'_>, source_text: &str) -> bool { + parameters_have_equal_sigils(first, second) + && types_are_equal( + parameter_type_annotation(first), + parameter_type_annotation(second), + source_text, + ) +} + +fn types_are_equal( + first: Option<&TSTypeAnnotation<'_>>, + second: Option<&TSTypeAnnotation<'_>>, + source_text: &str, +) -> bool { + match (first, second) { + (None, None) => true, + (Some(first), Some(second)) => { + first.type_annotation.span().source_text(source_text) + == second.type_annotation.span().source_text(source_text) + } + _ => false, + } +} + +fn get_index_of_first_difference( + first: &[T], + second: &[T], + mut equal: impl FnMut(&T, &T) -> bool, +) -> Option { + (0..first.len().min(second.len())).find(|&i| !equal(&first[i], &second[i])) +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + ( + " + function g(): void; + function g(a: number, b: number): void; + function g(a?: number, b?: number): void {} + ", + None, + ), + ( + " + function rest(...xs: number[]): void; + function rest(xs: number[], y: string): void; + function rest(...args: any[]) {} + ", + None, + ), + ( + " + class C { + constructor(); + constructor(a: number, b: number); + constructor(a?: number, b?: number) {} + + a(): void; + a(a: number, b: number): void; + a(a?: number, b?: number): void {} + } + ", + None, + ), + ( + " + declare class Example { + privateMethod(a: number): void; + #privateMethod(a: number, b?: string): void; + } + ", + None, + ), + ( + " + declare class Example { + #privateMethod1(a: number): void; + #privateMethod2(a: number, b?: string): void; + } + ", + None, + ), + ( + " + interface I { + a2(): void; + a2(x: number, y: number): void; + } + ", + None, + ), + ( + " + interface I { + a4(): void; + a4(x: number): number; + } + ", + None, + ), + ( + " + interface I { + a5(x: T): T; + a5(x: number): number; + } + ", + None, + ), + ( + " + interface I { + b2(x: string): void; + b2(...x: number[]): void; + } + ", + None, + ), + ( + " + interface I { + b3(...x: number[]): void; + b3(...x: string[]): void; + } + ", + None, + ), + ( + " + interface I { + c3(x: number): void; + c3(x?: string): void; + } + ", + None, + ), + ( + " + interface I { + d2(x: string, y: number): void; + d2(x: number, y: string): void; + } + ", + None, + ), + ( + " + declare class D { + static a(); + a(x: number); + } + ", + None, + ), + ( + " + interface Generic { + x(): void; + x(x: T[]): void; + } + ", + None, + ), + ( + " + interface I { + f(x1: number): void; + f(x1: boolean, x2?: number): void; + } + ", + None, + ), + ( + " + function f(x: T[]): void; + function f(x: T): void; + ", + None, + ), + ( + " + declare function foo(n: number): number; + + declare module 'hello' { + function foo(n: number, s: string): number; + } + ", + None, + ), + ( + " + { + function block(): number; + function block(n: number): number; + function block(n?: number): number { + return 3; + } + } + ", + None, + ), + ( + " + export interface Foo { + bar(baz: string): number[]; + bar(): string[]; + } + ", + None, + ), + ( + " + declare module 'foo' { + export default function (foo: number): string[]; + } + ", + None, + ), + ( + " + export default function (foo: number): string[]; + ", + None, + ), + ( + " + function p(key: string): Promise; + function p(key: string, defaultValue: string): Promise; + function p(key: string, defaultValue?: string): Promise { + const obj: Record = {}; + return obj[key] || defaultValue; + } + ", + None, + ), + ( + " + interface I { + p(x: T): Promise; + p(x: number): Promise; + } + ", + None, + ), + ( + " + function rest(...xs: number[]): Promise; + function rest(xs: number[], y: string): Promise; + async function rest(...args: any[]): Promise { + const y = args[1] as string | undefined; + return y || args; + } + ", + None, + ), + ( + " + declare class Foo { + get bar(); + set bar(x: number); + } + ", + None, + ), + ( + " + interface Foo { + get bar(); + set bar(x: number); + } + ", + None, + ), + ( + " + abstract class Foo { + abstract get bar(); + abstract set bar(a: unknown); + } + ", + None, + ), + ( + " + function f(a: number): void; + function f(b: string): void; + function f(a: number | string): void {} + ", + Some(serde_json::json!([{ "ignoreDifferentlyNamedParameters": true }])), + ), + ( + " + function f(m: number): void; + function f(v: number, u: string): void; + function f(v: number, u?: string): void {} + ", + Some(serde_json::json!([{ "ignoreDifferentlyNamedParameters": true }])), + ), + ( + " + function f(v: boolean): number; + function f(): string; + ", + Some(serde_json::json!([{ "ignoreDifferentlyNamedParameters": true }])), + ), + ( + " + function f(v: boolean, u: boolean): number; + function f(v: boolean): string; + ", + Some(serde_json::json!([{ "ignoreDifferentlyNamedParameters": true }])), + ), + ( + " + function f(v: number, u?: string): void {} + function f(v: number): void; + function f(): string; + ", + Some(serde_json::json!([{ "ignoreDifferentlyNamedParameters": true }])), + ), + ( + " + function f(a: boolean, ...c: number[]): void; + function f(a: boolean, ...d: string[]): void; + function f(a: boolean, ...c: (number | string)[]): void {} + ", + Some(serde_json::json!([{ "ignoreDifferentlyNamedParameters": true }])), + ), + ( + " + class C { + constructor(); + constructor(a: number, b: number); + constructor(c?: number, b?: number) {} + + a(): void; + a(a: number, b: number): void; + a(a?: number, d?: number): void {} + } + ", + Some(serde_json::json!([{ "ignoreDifferentlyNamedParameters": true }])), + ), + ( + " + /** @deprecated */ + declare function f(x: number): unknown; + declare function f(x: boolean): unknown; + ", + Some(serde_json::json!([{ "ignoreOverloadsWithDifferentJSDoc": true }])), + ), + ( + " + declare function f(x: number): unknown; + /** @deprecated */ + declare function f(x: boolean): unknown; + ", + Some(serde_json::json!([{ "ignoreOverloadsWithDifferentJSDoc": true }])), + ), + ( + " + declare function f(x: number): unknown; + /** @deprecated */ declare function f(x: boolean): unknown; + ", + Some(serde_json::json!([{ "ignoreOverloadsWithDifferentJSDoc": true }])), + ), + ( + " + declare function f(x: string): void; + /** + * @async + */ + declare function f(x: boolean): void; + /** + * @deprecate + */ + declare function f(x: number): void; + ", + Some(serde_json::json!([{ "ignoreOverloadsWithDifferentJSDoc": true }])), + ), + ( + " + /** + * @deprecate + */ + declare function f(x: string): void; + /** + * @async + */ + declare function f(x: boolean): void; + declare function f(x: number): void; + ", + Some(serde_json::json!([{ "ignoreOverloadsWithDifferentJSDoc": true }])), + ), + ( + " + /** + * This signature does something. + */ + declare function f(x: number): void; + + /** + * This signature does something else. + */ + declare function f(x: string): void; + ", + Some(serde_json::json!([{ "ignoreOverloadsWithDifferentJSDoc": true }])), + ), + ( + " + /** @deprecated */ + export function f(x: number): unknown; + export function f(x: boolean): unknown; + ", + Some(serde_json::json!([{ "ignoreOverloadsWithDifferentJSDoc": true }])), + ), + ( + " + /** + * This signature does something. + */ + + // some other comment + export function f(x: number): void; + + /** + * This signature does something else. + */ + export function f(x: string): void; + ", + Some(serde_json::json!([{ "ignoreOverloadsWithDifferentJSDoc": true }])), + ), + ( + " + interface I { + /** + * This signature does something else. + */ + f(x: number): void; + f(x: string): void; + } + ", + Some(serde_json::json!([{ "ignoreOverloadsWithDifferentJSDoc": true }])), + ), + ( + " + /* @deprecated */ + declare function f(x: number): unknown; + declare function f(x: boolean): unknown; + ", + Some(serde_json::json!([{ "ignoreOverloadsWithDifferentJSDoc": true }])), + ), + ( + " + /* + * This signature does something. + */ + declare function f(x: number): unknown; + declare function f(x: boolean): unknown; + ", + Some(serde_json::json!([{ "ignoreOverloadsWithDifferentJSDoc": true }])), + ), + ( + " + /** + * This signature does something. + **/ + declare function f(x: number): unknown; + declare function f(x: boolean): unknown; + ", + Some(serde_json::json!([{ "ignoreOverloadsWithDifferentJSDoc": true }])), + ), + ( + " + class C { + a(b: string): void; + /** + * @deprecate + */ + a(b: number): void; + } + ", + Some(serde_json::json!([{ "ignoreOverloadsWithDifferentJSDoc": true }])), + ), + ( + " + function f(): void; + function f(this: {}): void; + function f(this: void | {}): void {} + ", + None, + ), + ( + " + function f(a: boolean): void; + function f(this: {}, a: boolean): void; + function f(this: void | {}, a: boolean): void {} + ", + None, + ), + ( + " + function f(this: void, a: boolean): void; + function f(this: {}, a: boolean): void; + function f(this: void | {}, a: boolean): void {} + ", + None, + ), + ]; + + let fail = vec![ + ( + " + function f(a: number): void; + function f(b: string): void; + function f(a: number | string): void {} + ", + None, + ), + ( + " + function f(x: number): void; + function f(x: string): void; + function f(x: any): any { + return x; + } + ", + None, + ), + ( + " + function f(x: number): void; + function f(x: string): void; + function f(x: any): any { + return x; + } + ", + Some(serde_json::json!([{ "ignoreDifferentlyNamedParameters": true }])), + ), + ( + " + function opt(xs?: number[]): void; + function opt(xs: number[], y: string): void; + function opt(...args: any[]) {} + ", + None, + ), + ( + " + interface I { + a0(): void; + a0(x: string): string; + a0(x: number): void; + } + ", + None, + ), + ( + " + interface I { + a0(): void; + a0(x: string): string; + a0(x: number): void; + } + ", + Some(serde_json::json!([{ "ignoreDifferentlyNamedParameters": true }])), + ), + ( + " + interface I { + a1(): void; + a1(x: number): void; + } + ", + None, + ), + ( + " + interface I { + a3(): void; + a3(x: number, y?: number, ...z: number[]): void; + } + ", + None, + ), + ( + " + interface I { + b(): void; + b(...x: number[]): void; + } + ", + None, + ), + ( + " + interface I { + c(): void; + c(x?: number): void; + } + ", + None, + ), + ( + " + interface I { + c2(x?: number): void; + c2(x?: string): void; + } + ", + None, + ), + ( + " + interface I { + d(x: number): void; + d(x: string): void; + } + ", + None, + ), + ( + " + type T = { + (): void; + (x: number): void; + }; + ", + None, + ), + ( + " + declare class Example { + #privateMethod(a: number): void; + #privateMethod(a: number, b?: string): void; + } + ", + None, + ), + ( + " + declare class C { + constructor(); + constructor(x: number); + } + ", + None, + ), + ( + " + interface I { + f(x: number); + f(x: string | boolean); + } + ", + None, + ), + ( + " + interface I { + f(x: number); + f(x: [string, boolean]); + } + ", + None, + ), + ( + " + interface Generic { + y(x: T[]): void; + y(x: T): void; + } + ", + None, + ), + ( + " + function f(x: T[]): void; + function f(x: T): void; + ", + None, + ), + ( + " + function f(x: T[]): void; + function f(x: T): void; + ", + None, + ), + ( + " + abstract class Foo { + public abstract f(x: number): void; + public abstract f(x: string): void; + } + ", + None, + ), + ( + " + abstract class C { + a(b: string): void; + /** + * @deprecate + */ + a(b: number): void; + } + ", + None, + ), + ( + " + interface Foo { + 'f'(x: string): void; + 'f'(x: number): void; + } + ", + None, + ), + ( + " + interface Foo { + new (x: string): Foo; + new (x: number): Foo; + } + ", + None, + ), + ( + " + enum Enum { + Func = 'function', + } + + interface IFoo { + [Enum.Func](x: string): void; + [Enum.Func](x: number): void; + } + ", + None, + ), + ( + " + export function foo(line: number): number; + export function foo(line: number, character?: number): number; + ", + None, + ), + ( + " + declare function foo(line: number): number; + export function foo(line: number, character?: number): number; + ", + None, + ), + ( + " + declare module 'foo' { + export default function (foo: number): string[]; + export default function (foo: number, bar?: string): string[]; + } + ", + None, + ), + ( + " + export default function (foo: number): string[]; + export default function (foo: number, bar?: string): string[]; + ", + None, + ), + ( + " + /** + * @deprecate + */ + declare function f(x: string): void; + declare function f(x: number): void; + declare function f(x: boolean): void; + ", + Some(serde_json::json!([{ "ignoreOverloadsWithDifferentJSDoc": true }])), + ), + ( + " + /** + * @deprecate + */ + declare function f(x: string): void; + /** + * @deprecate + */ + declare function f(x: number): void; + declare function f(x: boolean): void; + ", + Some(serde_json::json!([{ "ignoreOverloadsWithDifferentJSDoc": true }])), + ), + ( + " + declare function f(x: string): void; + /** + * @deprecate + */ + declare function f(x: number): void; + /** + * @deprecate + */ + declare function f(x: boolean): void; + ", + Some(serde_json::json!([{ "ignoreOverloadsWithDifferentJSDoc": true }])), + ), + ( + " + export function f(x: string): void; + /** + * @deprecate + */ + export function f(x: number): void; + /** + * @deprecate + */ + export function f(x: boolean): void; + ", + Some(serde_json::json!([{ "ignoreOverloadsWithDifferentJSDoc": true }])), + ), + ( + " + /** + * This signature does something. + */ + + /** + * This signature does something else. + */ + function f(x: number): void; + + /** + * This signature does something else. + */ + function f(x: string): void; + ", + Some(serde_json::json!([{ "ignoreOverloadsWithDifferentJSDoc": true }])), + ), + ( + " + interface I { + f(x: string): void; + /** + * @deprecate + */ + f(x: number): void; + /** + * @deprecate + */ + f(x: boolean): void; + } + ", + Some(serde_json::json!([{ "ignoreOverloadsWithDifferentJSDoc": true }])), + ), + ( + " + // a line comment + declare function f(x: number): unknown; + declare function f(x: boolean): unknown; + ", + Some(serde_json::json!([{ "ignoreOverloadsWithDifferentJSDoc": true }])), + ), + ( + " + function f(this: {}, a: boolean): void; + function f(this: {}, a: string): void; + function f(this: {}, a: boolean | string): void {} + ", + None, + ), + ( + " + function f(this: {}): void; + function f(this: {}, a: string): void; + function f(this: {}, a?: string): void {} + ", + None, + ), + ( + " + function f(this: string): void; + function f(this: number): void; + function f(this: string | number): void {} + ", + None, + ), + ( + " + function f(this: string, a: boolean): void; + function f(this: number, a: boolean): void; + function f(this: string | number, a: boolean): void {} + ", + None, + ), + ]; + + Tester::new(UnifiedSignatures::NAME, UnifiedSignatures::PLUGIN, pass, fail).test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/typescript_unified_signatures.snap b/crates/oxc_linter/src/snapshots/typescript_unified_signatures.snap new file mode 100644 index 0000000000000..0786c6ffd367a --- /dev/null +++ b/crates/oxc_linter/src/snapshots/typescript_unified_signatures.snap @@ -0,0 +1,478 @@ +--- +source: crates/oxc_linter/src/tester.rs +--- + + ⚠ typescript-eslint(unified-signatures): These overloads can be combined into one signature taking `number | string`. + ╭─[unified_signatures.tsx:3:24] + 1 │ + 2 │ function f(a: number): void; + · ────┬──── + · ╰── with this overload parameter + 3 │ function f(b: string): void; + · ────┬──── + · ╰── this parameter can be unified + 4 │ function f(a: number | string): void {} + ╰──── + + ⚠ typescript-eslint(unified-signatures): These overloads can be combined into one signature taking `number | string`. + ╭─[unified_signatures.tsx:3:24] + 1 │ + 2 │ function f(x: number): void; + · ────┬──── + · ╰── with this overload parameter + 3 │ function f(x: string): void; + · ────┬──── + · ╰── this parameter can be unified + 4 │ function f(x: any): any { + ╰──── + + ⚠ typescript-eslint(unified-signatures): These overloads can be combined into one signature taking `number | string`. + ╭─[unified_signatures.tsx:3:24] + 1 │ + 2 │ function f(x: number): void; + · ────┬──── + · ╰── with this overload parameter + 3 │ function f(x: string): void; + · ────┬──── + · ╰── this parameter can be unified + 4 │ function f(x: any): any { + ╰──── + + ⚠ typescript-eslint(unified-signatures): These overloads can be combined into one signature with an optional parameter. + ╭─[unified_signatures.tsx:3:40] + 1 │ + 2 │ function opt(xs?: number[]): void; + · ──────┬────── + · ╰── matching overload ends here + 3 │ function opt(xs: number[], y: string): void; + · ────┬──── + · ╰── this parameter only appears in one overload + 4 │ function opt(...args: any[]) {} + ╰──── + + ⚠ typescript-eslint(unified-signatures): This overload can be combined with another overload into one signature with an optional parameter. + ╭─[unified_signatures.tsx:5:18] + 4 │ a0(x: string): string; + 5 │ a0(x: number): void; + · ────┬──── + · ╰── this parameter only appears in one overload + 6 │ } + ╰──── + + ⚠ typescript-eslint(unified-signatures): This overload can be combined with another overload into one signature with an optional parameter. + ╭─[unified_signatures.tsx:5:18] + 4 │ a0(x: string): string; + 5 │ a0(x: number): void; + · ────┬──── + · ╰── this parameter only appears in one overload + 6 │ } + ╰──── + + ⚠ typescript-eslint(unified-signatures): These overloads can be combined into one signature with an optional parameter. + ╭─[unified_signatures.tsx:4:18] + 3 │ a1(): void; + 4 │ a1(x: number): void; + · ────┬──── + · ╰── this parameter only appears in one overload + 5 │ } + ╰──── + + ⚠ typescript-eslint(unified-signatures): These overloads can be combined into one signature with a rest parameter. + ╭─[unified_signatures.tsx:4:41] + 3 │ a3(): void; + 4 │ a3(x: number, y?: number, ...z: number[]): void; + · ───────┬────── + · ╰── this parameter only appears in one overload + 5 │ } + ╰──── + + ⚠ typescript-eslint(unified-signatures): These overloads can be combined into one signature with a rest parameter. + ╭─[unified_signatures.tsx:4:17] + 3 │ b(): void; + 4 │ b(...x: number[]): void; + · ───────┬────── + · ╰── this parameter only appears in one overload + 5 │ } + ╰──── + + ⚠ typescript-eslint(unified-signatures): These overloads can be combined into one signature with an optional parameter. + ╭─[unified_signatures.tsx:4:17] + 3 │ c(): void; + 4 │ c(x?: number): void; + · ─────┬──── + · ╰── this parameter only appears in one overload + 5 │ } + ╰──── + + ⚠ typescript-eslint(unified-signatures): These overloads can be combined into one signature taking `number | string`. + ╭─[unified_signatures.tsx:4:18] + 2 │ interface I { + 3 │ c2(x?: number): void; + · ─────┬──── + · ╰── with this overload parameter + 4 │ c2(x?: string): void; + · ─────┬──── + · ╰── this parameter can be unified + 5 │ } + ╰──── + + ⚠ typescript-eslint(unified-signatures): These overloads can be combined into one signature taking `number | string`. + ╭─[unified_signatures.tsx:4:17] + 2 │ interface I { + 3 │ d(x: number): void; + · ────┬──── + · ╰── with this overload parameter + 4 │ d(x: string): void; + · ────┬──── + · ╰── this parameter can be unified + 5 │ } + ╰──── + + ⚠ typescript-eslint(unified-signatures): These overloads can be combined into one signature with an optional parameter. + ╭─[unified_signatures.tsx:4:16] + 3 │ (): void; + 4 │ (x: number): void; + · ────┬──── + · ╰── this parameter only appears in one overload + 5 │ }; + ╰──── + + ⚠ typescript-eslint(unified-signatures): These overloads can be combined into one signature with an optional parameter. + ╭─[unified_signatures.tsx:4:41] + 2 │ declare class Example { + 3 │ #privateMethod(a: number): void; + · ────┬──── + · ╰── matching overload ends here + 4 │ #privateMethod(a: number, b?: string): void; + · ─────┬──── + · ╰── this parameter only appears in one overload + 5 │ } + ╰──── + + ⚠ typescript-eslint(unified-signatures): These overloads can be combined into one signature with an optional parameter. + ╭─[unified_signatures.tsx:4:27] + 3 │ constructor(); + 4 │ constructor(x: number); + · ────┬──── + · ╰── this parameter only appears in one overload + 5 │ } + ╰──── + + ⚠ typescript-eslint(unified-signatures): These overloads can be combined into one signature taking `number | string | boolean`. + ╭─[unified_signatures.tsx:4:17] + 2 │ interface I { + 3 │ f(x: number); + · ────┬──── + · ╰── with this overload parameter + 4 │ f(x: string | boolean); + · ─────────┬───────── + · ╰── this parameter can be unified + 5 │ } + ╰──── + + ⚠ typescript-eslint(unified-signatures): These overloads can be combined into one signature taking `number | [string, boolean]`. + ╭─[unified_signatures.tsx:4:17] + 2 │ interface I { + 3 │ f(x: number); + · ────┬──── + · ╰── with this overload parameter + 4 │ f(x: [string, boolean]); + · ──────────┬───────── + · ╰── this parameter can be unified + 5 │ } + ╰──── + + ⚠ typescript-eslint(unified-signatures): These overloads can be combined into one signature taking `T[] | T`. + ╭─[unified_signatures.tsx:4:17] + 2 │ interface Generic { + 3 │ y(x: T[]): void; + · ───┬── + · ╰── with this overload parameter + 4 │ y(x: T): void; + · ──┬─ + · ╰── this parameter can be unified + 5 │ } + ╰──── + + ⚠ typescript-eslint(unified-signatures): These overloads can be combined into one signature taking `T[] | T`. + ╭─[unified_signatures.tsx:3:27] + 1 │ + 2 │ function f(x: T[]): void; + · ───┬── + · ╰── with this overload parameter + 3 │ function f(x: T): void; + · ──┬─ + · ╰── this parameter can be unified + 4 │ + ╰──── + + ⚠ typescript-eslint(unified-signatures): These overloads can be combined into one signature taking `T[] | T`. + ╭─[unified_signatures.tsx:3:42] + 1 │ + 2 │ function f(x: T[]): void; + · ───┬── + · ╰── with this overload parameter + 3 │ function f(x: T): void; + · ──┬─ + · ╰── this parameter can be unified + 4 │ + ╰──── + + ⚠ typescript-eslint(unified-signatures): These overloads can be combined into one signature taking `number | string`. + ╭─[unified_signatures.tsx:4:33] + 2 │ abstract class Foo { + 3 │ public abstract f(x: number): void; + · ────┬──── + · ╰── with this overload parameter + 4 │ public abstract f(x: string): void; + · ────┬──── + · ╰── this parameter can be unified + 5 │ } + ╰──── + + ⚠ typescript-eslint(unified-signatures): These overloads can be combined into one signature taking `string | number`. + ╭─[unified_signatures.tsx:7:17] + 2 │ abstract class C { + 3 │ a(b: string): void; + · ────┬──── + · ╰── with this overload parameter + 4 │ /** + 5 │ * @deprecate + 6 │ */ + 7 │ a(b: number): void; + · ────┬──── + · ╰── this parameter can be unified + 8 │ } + ╰──── + + ⚠ typescript-eslint(unified-signatures): These overloads can be combined into one signature taking `string | number`. + ╭─[unified_signatures.tsx:4:19] + 2 │ interface Foo { + 3 │ 'f'(x: string): void; + · ────┬──── + · ╰── with this overload parameter + 4 │ 'f'(x: number): void; + · ────┬──── + · ╰── this parameter can be unified + 5 │ } + ╰──── + + ⚠ typescript-eslint(unified-signatures): These overloads can be combined into one signature taking `string | number`. + ╭─[unified_signatures.tsx:4:20] + 2 │ interface Foo { + 3 │ new (x: string): Foo; + · ────┬──── + · ╰── with this overload parameter + 4 │ new (x: number): Foo; + · ────┬──── + · ╰── this parameter can be unified + 5 │ } + ╰──── + + ⚠ typescript-eslint(unified-signatures): These overloads can be combined into one signature taking `string | number`. + ╭─[unified_signatures.tsx:8:27] + 6 │ interface IFoo { + 7 │ [Enum.Func](x: string): void; + · ────┬──── + · ╰── with this overload parameter + 8 │ [Enum.Func](x: number): void; + · ────┬──── + · ╰── this parameter can be unified + 9 │ } + ╰──── + + ⚠ typescript-eslint(unified-signatures): These overloads can be combined into one signature with an optional parameter. + ╭─[unified_signatures.tsx:3:47] + 1 │ + 2 │ export function foo(line: number): number; + · ──────┬───── + · ╰── matching overload ends here + 3 │ export function foo(line: number, character?: number): number; + · ─────────┬──────── + · ╰── this parameter only appears in one overload + 4 │ + ╰──── + + ⚠ typescript-eslint(unified-signatures): These overloads can be combined into one signature with an optional parameter. + ╭─[unified_signatures.tsx:3:47] + 1 │ + 2 │ declare function foo(line: number): number; + · ──────┬───── + · ╰── matching overload ends here + 3 │ export function foo(line: number, character?: number): number; + · ─────────┬──────── + · ╰── this parameter only appears in one overload + 4 │ + ╰──── + + ⚠ typescript-eslint(unified-signatures): These overloads can be combined into one signature with an optional parameter. + ╭─[unified_signatures.tsx:4:53] + 2 │ declare module 'foo' { + 3 │ export default function (foo: number): string[]; + · ─────┬───── + · ╰── matching overload ends here + 4 │ export default function (foo: number, bar?: string): string[]; + · ──────┬───── + · ╰── this parameter only appears in one overload + 5 │ } + ╰──── + + ⚠ typescript-eslint(unified-signatures): These overloads can be combined into one signature with an optional parameter. + ╭─[unified_signatures.tsx:3:51] + 1 │ + 2 │ export default function (foo: number): string[]; + · ─────┬───── + · ╰── matching overload ends here + 3 │ export default function (foo: number, bar?: string): string[]; + · ──────┬───── + · ╰── this parameter only appears in one overload + 4 │ + ╰──── + + ⚠ typescript-eslint(unified-signatures): This overload can be combined with another overload into one signature taking `number | boolean`. + ╭─[unified_signatures.tsx:7:32] + 5 │ declare function f(x: string): void; + 6 │ declare function f(x: number): void; + · ────┬──── + · ╰── with this overload parameter + 7 │ declare function f(x: boolean): void; + · ─────┬──── + · ╰── this parameter can be unified + 8 │ + ╰──── + + ⚠ typescript-eslint(unified-signatures): This overload can be combined with another overload into one signature taking `string | number`. + ╭─[unified_signatures.tsx:9:32] + 4 │ */ + 5 │ declare function f(x: string): void; + · ────┬──── + · ╰── with this overload parameter + 6 │ /** + 7 │ * @deprecate + 8 │ */ + 9 │ declare function f(x: number): void; + · ────┬──── + · ╰── this parameter can be unified + 10 │ declare function f(x: boolean): void; + ╰──── + + ⚠ typescript-eslint(unified-signatures): This overload can be combined with another overload into one signature taking `number | boolean`. + ╭─[unified_signatures.tsx:10:32] + 5 │ */ + 6 │ declare function f(x: number): void; + · ────┬──── + · ╰── with this overload parameter + 7 │ /** + 8 │ * @deprecate + 9 │ */ + 10 │ declare function f(x: boolean): void; + · ─────┬──── + · ╰── this parameter can be unified + 11 │ + ╰──── + + ⚠ typescript-eslint(unified-signatures): This overload can be combined with another overload into one signature taking `number | boolean`. + ╭─[unified_signatures.tsx:10:31] + 5 │ */ + 6 │ export function f(x: number): void; + · ────┬──── + · ╰── with this overload parameter + 7 │ /** + 8 │ * @deprecate + 9 │ */ + 10 │ export function f(x: boolean): void; + · ─────┬──── + · ╰── this parameter can be unified + 11 │ + ╰──── + + ⚠ typescript-eslint(unified-signatures): These overloads can be combined into one signature taking `number | string`. + ╭─[unified_signatures.tsx:14:24] + 8 │ */ + 9 │ function f(x: number): void; + · ────┬──── + · ╰── with this overload parameter + 10 │ + 11 │ /** + 12 │ * This signature does something else. + 13 │ */ + 14 │ function f(x: string): void; + · ────┬──── + · ╰── this parameter can be unified + 15 │ + ╰──── + + ⚠ typescript-eslint(unified-signatures): This overload can be combined with another overload into one signature taking `number | boolean`. + ╭─[unified_signatures.tsx:11:17] + 6 │ */ + 7 │ f(x: number): void; + · ────┬──── + · ╰── with this overload parameter + 8 │ /** + 9 │ * @deprecate + 10 │ */ + 11 │ f(x: boolean): void; + · ─────┬──── + · ╰── this parameter can be unified + 12 │ } + ╰──── + + ⚠ typescript-eslint(unified-signatures): These overloads can be combined into one signature taking `number | boolean`. + ╭─[unified_signatures.tsx:4:32] + 2 │ // a line comment + 3 │ declare function f(x: number): unknown; + · ────┬──── + · ╰── with this overload parameter + 4 │ declare function f(x: boolean): unknown; + · ─────┬──── + · ╰── this parameter can be unified + 5 │ + ╰──── + + ⚠ typescript-eslint(unified-signatures): These overloads can be combined into one signature taking `boolean | string`. + ╭─[unified_signatures.tsx:3:34] + 1 │ + 2 │ function f(this: {}, a: boolean): void; + · ─────┬──── + · ╰── with this overload parameter + 3 │ function f(this: {}, a: string): void; + · ────┬──── + · ╰── this parameter can be unified + 4 │ function f(this: {}, a: boolean | string): void {} + ╰──── + + ⚠ typescript-eslint(unified-signatures): These overloads can be combined into one signature with an optional parameter. + ╭─[unified_signatures.tsx:3:34] + 1 │ + 2 │ function f(this: {}): void; + · ────┬─── + · ╰── matching overload ends here + 3 │ function f(this: {}, a: string): void; + · ────┬──── + · ╰── this parameter only appears in one overload + 4 │ function f(this: {}, a?: string): void {} + ╰──── + + ⚠ typescript-eslint(unified-signatures): These overloads can be combined into one signature taking `string | number`. + ╭─[unified_signatures.tsx:3:24] + 1 │ + 2 │ function f(this: string): void; + · ──────┬───── + · ╰── with this overload parameter + 3 │ function f(this: number): void; + · ──────┬───── + · ╰── this parameter can be unified + 4 │ function f(this: string | number): void {} + ╰──── + + ⚠ typescript-eslint(unified-signatures): These overloads can be combined into one signature taking `string | number`. + ╭─[unified_signatures.tsx:3:24] + 1 │ + 2 │ function f(this: string, a: boolean): void; + · ──────┬───── + · ╰── with this overload parameter + 3 │ function f(this: number, a: boolean): void; + · ──────┬───── + · ╰── this parameter can be unified + 4 │ function f(this: string | number, a: boolean): void {} + ╰────