|
| 1 | +use crate::JsRuleAction; |
1 | 2 | use biome_analyze::{ |
2 | | - Ast, Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule, |
| 3 | + Ast, FixKind, Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule, |
3 | 4 | }; |
4 | 5 | use biome_console::markup; |
5 | | -use biome_js_syntax::{TsMethodSignatureTypeMember, TsPropertySignatureTypeMember}; |
6 | | -use biome_rowan::{AstNode, declare_node_union}; |
| 6 | +use biome_js_factory::make; |
| 7 | +use biome_js_syntax::{ |
| 8 | + AnyTsType, AnyTsTypeMember, T, TsMethodSignatureTypeMember, TsPropertySignatureTypeMember, |
| 9 | + TsTypeMemberList, |
| 10 | +}; |
| 11 | +use biome_rowan::{AstNode, BatchMutationExt, TriviaPieceKind, declare_node_union}; |
7 | 12 | use biome_rule_options::use_consistent_method_signatures::{ |
8 | 13 | MethodSignatureStyle, UseConsistentMethodSignaturesOptions, |
9 | 14 | }; |
@@ -181,11 +186,8 @@ declare_lint_rule! { |
181 | 186 | name: "useConsistentMethodSignatures", |
182 | 187 | language: "ts", |
183 | 188 | recommended: false, |
184 | | - issue_number: Some("8780"), |
185 | 189 | sources: &[RuleSource::EslintTypeScript("method-signature-style").same()], |
186 | | - // TODO: Implement fix to convert between method/property |
187 | | - // This will need to handle transforming overloads into intersections of function properties |
188 | | - // fix_kind: FixKind::Unsafe, |
| 190 | + fix_kind: FixKind::Unsafe, |
189 | 191 | } |
190 | 192 | } |
191 | 193 |
|
@@ -247,6 +249,157 @@ impl Rule for UseConsistentMethodSignatures { |
247 | 249 |
|
248 | 250 | Some(diagnostic) |
249 | 251 | } |
| 252 | + |
| 253 | + fn action( |
| 254 | + ctx: &RuleContext<Self>, |
| 255 | + state: &Self::State, |
| 256 | + ) -> Option<JsRuleAction> { |
| 257 | + let node = ctx.query(); |
| 258 | + let mut mutation = ctx.root().begin(); |
| 259 | + |
| 260 | + let (prev, next) = match node { |
| 261 | + AnyTsMethodSignatureLike::TsMethodSignatureTypeMember(method) => { |
| 262 | + let new_node = method_to_property(method)?; |
| 263 | + ( |
| 264 | + AnyTsTypeMember::from(method.clone()), |
| 265 | + AnyTsTypeMember::from(new_node), |
| 266 | + ) |
| 267 | + } |
| 268 | + AnyTsMethodSignatureLike::TsPropertySignatureTypeMember(prop) => { |
| 269 | + let new_node = property_to_method(prop)?; |
| 270 | + ( |
| 271 | + AnyTsTypeMember::from(prop.clone()), |
| 272 | + AnyTsTypeMember::from(new_node), |
| 273 | + ) |
| 274 | + } |
| 275 | + }; |
| 276 | + |
| 277 | + mutation.replace_node(prev, next); |
| 278 | + |
| 279 | + Some(JsRuleAction::new( |
| 280 | + ctx.metadata().action_category(ctx.category(), ctx.group()), |
| 281 | + ctx.metadata().applicability(), |
| 282 | + markup! { "Convert to "<Emphasis>{state.target_style}</Emphasis>"-style signature." } |
| 283 | + .to_owned(), |
| 284 | + mutation, |
| 285 | + )) |
| 286 | + } |
| 287 | +} |
| 288 | + |
| 289 | +/// Converts a method-style signature into a property-style one. |
| 290 | +/// |
| 291 | +/// Returns `None` if: |
| 292 | +/// - The method has no explicit return type annotation (can't build a valid function type). |
| 293 | +/// - The method is one of several overloads (would need intersection types — handled separately). |
| 294 | +fn method_to_property( |
| 295 | + node: &TsMethodSignatureTypeMember, |
| 296 | +) -> Option<TsPropertySignatureTypeMember> { |
| 297 | + let return_type_annotation = node.return_type_annotation()?; |
| 298 | + |
| 299 | + if has_method_overloads(node) { |
| 300 | + return None; |
| 301 | + } |
| 302 | + |
| 303 | + let name = node.name().ok()?; |
| 304 | + let orig_params = node.parameters().ok()?; |
| 305 | + // Rebuild parameters with a `)` that has trailing whitespace so the output is |
| 306 | + // `(arg: string) => void` instead of `(arg: string)=> void`. |
| 307 | + let parameters = make::js_parameters( |
| 308 | + orig_params.l_paren_token().ok()?, |
| 309 | + orig_params.items(), |
| 310 | + make::token(T![')']).with_trailing_trivia([(TriviaPieceKind::Whitespace, " ")]), |
| 311 | + ); |
| 312 | + let return_type = return_type_annotation.ty().ok()?; |
| 313 | + |
| 314 | + let function_type = make::ts_function_type( |
| 315 | + parameters, |
| 316 | + make::token(T![=>]).with_trailing_trivia([(TriviaPieceKind::Whitespace, " ")]), |
| 317 | + return_type, |
| 318 | + ) |
| 319 | + .build() |
| 320 | + .with_type_parameters(node.type_parameters()); |
| 321 | + |
| 322 | + let type_annotation = make::ts_type_annotation( |
| 323 | + make::token(T![:]).with_trailing_trivia([(TriviaPieceKind::Whitespace, " ")]), |
| 324 | + AnyTsType::from(function_type), |
| 325 | + ); |
| 326 | + |
| 327 | + let mut builder = make::ts_property_signature_type_member(name) |
| 328 | + .with_type_annotation(type_annotation); |
| 329 | + |
| 330 | + if let Some(opt) = node.optional_token() { |
| 331 | + builder = builder.with_optional_token(opt); |
| 332 | + } |
| 333 | + if let Some(sep) = node.separator_token() { |
| 334 | + builder = builder.with_separator_token_token(sep); |
| 335 | + } |
| 336 | + |
| 337 | + Some(builder.build()) |
| 338 | +} |
| 339 | + |
| 340 | +/// Converts a property-style signature into a method-style one. |
| 341 | +/// |
| 342 | +/// Returns `None` if the property has a `readonly` modifier (methods can't be readonly). |
| 343 | +fn property_to_method( |
| 344 | + node: &TsPropertySignatureTypeMember, |
| 345 | +) -> Option<TsMethodSignatureTypeMember> { |
| 346 | + if node.readonly_token().is_some() { |
| 347 | + return None; |
| 348 | + } |
| 349 | + |
| 350 | + let name = node.name().ok()?; |
| 351 | + let type_annotation = node.type_annotation()?; |
| 352 | + let function_type = type_annotation.ty().ok()?.as_ts_function_type()?.clone(); |
| 353 | + |
| 354 | + let orig_params = function_type.parameters().ok()?; |
| 355 | + // Rebuild parameters with a clean `)` — the original `)` carries trailing whitespace |
| 356 | + // from the source (e.g. the space before `=>` in `(arg: string) => void`), which would |
| 357 | + // produce `method() : void` instead of the desired `method(): void`. |
| 358 | + let parameters = make::js_parameters( |
| 359 | + orig_params.l_paren_token().ok()?, |
| 360 | + orig_params.items(), |
| 361 | + make::token(T![')']), |
| 362 | + ); |
| 363 | + let return_type = function_type.return_type().ok()?; |
| 364 | + |
| 365 | + let return_type_annotation = make::ts_return_type_annotation( |
| 366 | + make::token(T![:]).with_trailing_trivia([(TriviaPieceKind::Whitespace, " ")]), |
| 367 | + return_type, |
| 368 | + ); |
| 369 | + |
| 370 | + let mut builder = make::ts_method_signature_type_member(name, parameters) |
| 371 | + .with_return_type_annotation(return_type_annotation); |
| 372 | + |
| 373 | + if let Some(opt) = node.optional_token() { |
| 374 | + builder = builder.with_optional_token(opt); |
| 375 | + } |
| 376 | + if let Some(type_params) = function_type.type_parameters() { |
| 377 | + builder = builder.with_type_parameters(type_params); |
| 378 | + } |
| 379 | + if let Some(sep) = node.separator_token() { |
| 380 | + builder = builder.with_separator_token_token(sep); |
| 381 | + } |
| 382 | + |
| 383 | + Some(builder.build()) |
| 384 | +} |
| 385 | + |
| 386 | +/// Returns `true` if `node` is one of multiple overloads — i.e. the parent type member list |
| 387 | +/// contains more than one method signature with the same name. |
| 388 | +fn has_method_overloads(node: &TsMethodSignatureTypeMember) -> bool { |
| 389 | + check_method_overloads(node).unwrap_or(false) |
| 390 | +} |
| 391 | + |
| 392 | +fn check_method_overloads(node: &TsMethodSignatureTypeMember) -> Option<bool> { |
| 393 | + let name_text = node.name().ok()?.name()?; |
| 394 | + let member_list = node.parent::<TsTypeMemberList>()?; |
| 395 | + |
| 396 | + let overload_count = member_list |
| 397 | + .into_iter() |
| 398 | + .filter_map(|m| m.as_ts_method_signature_type_member().cloned()) |
| 399 | + .filter(|m| m.name().ok().and_then(|n| n.name()).is_some_and(|n| n == name_text)) |
| 400 | + .count(); |
| 401 | + |
| 402 | + Some(overload_count > 1) |
250 | 403 | } |
251 | 404 |
|
252 | 405 | declare_node_union! { |
|
0 commit comments