about summary refs log tree commit diff
diff options
context:
space:
mode:
authorbors[bot] <26634292+bors[bot]@users.noreply.github.com>2022-03-20 21:15:49 +0000
committerGitHub <noreply@github.com>2022-03-20 21:15:49 +0000
commitb594f9c441cf12319d10c14ba6a511d5c9db1b87 (patch)
treedad1a60d9be59b5902a2553984dbf27022cf237a
parent6f2b1186052761c8e07c334ec786800748c42f92 (diff)
parent1381a2de230e19944a71e8176936ac90764fbd6e (diff)
downloadrust-b594f9c441cf12319d10c14ba6a511d5c9db1b87.tar.gz
rust-b594f9c441cf12319d10c14ba6a511d5c9db1b87.zip
Merge #11690
11690: feat: Add an assist for inlining type aliases r=Veykril a=steven-joruk

I'm working towards implementing #10881, but I'd like to get this in first with earlier feedback.

Is `inline_type_alias` a good enough name? I guess the follow up assist would be called `inline_type_alias_into_all_users` based on that.

![valid_inlines](https://user-images.githubusercontent.com/1277939/158020510-fed78b5c-4c7e-46d1-9151-3044a29b9990.gif)

![invalid_inlines](https://user-images.githubusercontent.com/1277939/158020516-8a2deb6d-c6ec-4adf-a15b-c514fc97dc43.gif)



Co-authored-by: Steven Joruk <steven@joruk.com>
-rw-r--r--crates/ide_assists/src/handlers/inline_type_alias.rs758
-rw-r--r--crates/ide_assists/src/lib.rs4
-rw-r--r--crates/ide_assists/src/tests/generated.rs21
-rw-r--r--crates/syntax/src/ast/node_ext.rs9
4 files changed, 791 insertions, 1 deletions
diff --git a/crates/ide_assists/src/handlers/inline_type_alias.rs b/crates/ide_assists/src/handlers/inline_type_alias.rs
new file mode 100644
index 00000000000..eeb2e2e6679
--- /dev/null
+++ b/crates/ide_assists/src/handlers/inline_type_alias.rs
@@ -0,0 +1,758 @@
+// Some ideas for future improvements:
+// - Support replacing aliases which are used in expressions, e.g. `A::new()`.
+// - "inline_alias_to_users" assist #10881.
+// - Remove unused aliases if there are no longer any users, see inline_call.rs.
+
+use hir::PathResolution;
+use itertools::Itertools;
+use std::collections::HashMap;
+use syntax::{
+    ast::{
+        self,
+        make::{self},
+        HasGenericParams, HasName,
+    },
+    ted::{self},
+    AstNode, NodeOrToken, SyntaxNode,
+};
+
+use crate::{
+    assist_context::{AssistContext, Assists},
+    AssistId, AssistKind,
+};
+
+// Assist: inline_type_alias
+//
+// Replace a type alias with its concrete type.
+//
+// ```
+// type A<T = u32> = Vec<T>;
+//
+// fn main() {
+//     let a: $0A;
+// }
+// ```
+// ->
+// ```
+// type A<T = u32> = Vec<T>;
+//
+// fn main() {
+//     let a: Vec<u32>;
+// }
+// ```
+pub(crate) fn inline_type_alias(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
+    let alias_instance = ctx.find_node_at_offset::<ast::PathType>()?;
+    let alias = get_type_alias(&ctx, &alias_instance)?;
+    let concrete_type = alias.ty()?;
+
+    enum Replacement {
+        Generic { lifetime_map: LifetimeMap, const_and_type_map: ConstAndTypeMap },
+        Plain,
+    }
+
+    let replacement = if let Some(alias_generics) = alias.generic_param_list() {
+        if alias_generics.generic_params().next().is_none() {
+            cov_mark::hit!(no_generics_params);
+            return None;
+        }
+
+        let instance_args =
+            alias_instance.syntax().descendants().find_map(ast::GenericArgList::cast);
+
+        Replacement::Generic {
+            lifetime_map: LifetimeMap::new(&instance_args, &alias_generics)?,
+            const_and_type_map: ConstAndTypeMap::new(&instance_args, &alias_generics)?,
+        }
+    } else {
+        Replacement::Plain
+    };
+
+    let target = alias_instance.syntax().text_range();
+
+    acc.add(
+        AssistId("inline_type_alias", AssistKind::RefactorInline),
+        "Inline type alias",
+        target,
+        |builder| {
+            let replacement_text = match replacement {
+                Replacement::Generic { lifetime_map, const_and_type_map } => {
+                    create_replacement(&lifetime_map, &const_and_type_map, &concrete_type)
+                }
+                Replacement::Plain => concrete_type.to_string(),
+            };
+
+            builder.replace(target, replacement_text);
+        },
+    )
+}
+
+struct LifetimeMap(HashMap<String, ast::Lifetime>);
+
+impl LifetimeMap {
+    fn new(
+        instance_args: &Option<ast::GenericArgList>,
+        alias_generics: &ast::GenericParamList,
+    ) -> Option<Self> {
+        let mut inner = HashMap::new();
+
+        let wildcard_lifetime = make::lifetime("'_");
+        let lifetimes = alias_generics
+            .lifetime_params()
+            .filter_map(|lp| lp.lifetime())
+            .map(|l| l.to_string())
+            .collect_vec();
+
+        for lifetime in &lifetimes {
+            inner.insert(lifetime.to_string(), wildcard_lifetime.clone());
+        }
+
+        if let Some(instance_generic_args_list) = &instance_args {
+            for (index, lifetime) in instance_generic_args_list
+                .lifetime_args()
+                .filter_map(|arg| arg.lifetime())
+                .enumerate()
+            {
+                let key = match lifetimes.get(index) {
+                    Some(key) => key,
+                    None => {
+                        cov_mark::hit!(too_many_lifetimes);
+                        return None;
+                    }
+                };
+
+                inner.insert(key.clone(), lifetime);
+            }
+        }
+
+        Some(Self(inner))
+    }
+}
+
+struct ConstAndTypeMap(HashMap<String, SyntaxNode>);
+
+impl ConstAndTypeMap {
+    fn new(
+        instance_args: &Option<ast::GenericArgList>,
+        alias_generics: &ast::GenericParamList,
+    ) -> Option<Self> {
+        let mut inner = HashMap::new();
+        let instance_generics = generic_args_to_const_and_type_generics(instance_args);
+        let alias_generics = generic_param_list_to_const_and_type_generics(&alias_generics);
+
+        if instance_generics.len() > alias_generics.len() {
+            cov_mark::hit!(too_many_generic_args);
+            return None;
+        }
+
+        // Any declaration generics that don't have a default value must have one
+        // provided by the instance.
+        for (i, declaration_generic) in alias_generics.iter().enumerate() {
+            let key = declaration_generic.replacement_key()?;
+
+            if let Some(instance_generic) = instance_generics.get(i) {
+                inner.insert(key, instance_generic.replacement_value()?);
+            } else if let Some(value) = declaration_generic.replacement_value() {
+                inner.insert(key, value);
+            } else {
+                cov_mark::hit!(missing_replacement_param);
+                return None;
+            }
+        }
+
+        Some(Self(inner))
+    }
+}
+
+/// This doesn't attempt to ensure specified generics are compatible with those
+/// required by the type alias, other than lifetimes which must either all be
+/// specified or all omitted. It will replace TypeArgs with ConstArgs and vice
+/// versa if they're in the wrong position. It supports partially specified
+/// generics.
+///
+/// 1. Map the provided instance's generic args to the type alias's generic
+///    params:
+///
+///    ```
+///    type A<'a, const N: usize, T = u64> = &'a [T; N];
+///          ^ alias generic params
+///    let a: A<100>;
+///            ^ instance generic args
+///    ```
+///
+///    generic['a] = '_ due to omission
+///    generic[N] = 100 due to the instance arg
+///    generic[T] = u64 due to the default param
+///
+/// 2. Copy the concrete type and substitute in each found mapping:
+///
+///    &'_ [u64; 100]
+///
+/// 3. Remove wildcard lifetimes entirely:
+///
+///    &[u64; 100]
+fn create_replacement(
+    lifetime_map: &LifetimeMap,
+    const_and_type_map: &ConstAndTypeMap,
+    concrete_type: &ast::Type,
+) -> String {
+    let updated_concrete_type = concrete_type.clone_for_update();
+    let mut replacements = Vec::new();
+    let mut removals = Vec::new();
+
+    for syntax in updated_concrete_type.syntax().descendants() {
+        let syntax_string = syntax.to_string();
+        let syntax_str = syntax_string.as_str();
+
+        if let Some(old_lifetime) = ast::Lifetime::cast(syntax.clone()) {
+            if let Some(new_lifetime) = lifetime_map.0.get(&old_lifetime.to_string()) {
+                if new_lifetime.text() == "'_" {
+                    removals.push(NodeOrToken::Node(syntax.clone()));
+
+                    if let Some(ws) = syntax.next_sibling_or_token() {
+                        removals.push(ws.clone());
+                    }
+
+                    continue;
+                }
+
+                replacements.push((syntax.clone(), new_lifetime.syntax().clone_for_update()));
+            }
+        } else if let Some(replacement_syntax) = const_and_type_map.0.get(syntax_str) {
+            let new_string = replacement_syntax.to_string();
+            let new = if new_string == "_" {
+                make::wildcard_pat().syntax().clone_for_update()
+            } else {
+                replacement_syntax.clone_for_update()
+            };
+
+            replacements.push((syntax.clone(), new));
+        }
+    }
+
+    for (old, new) in replacements {
+        ted::replace(old, new);
+    }
+
+    for syntax in removals {
+        ted::remove(syntax);
+    }
+
+    updated_concrete_type.to_string()
+}
+
+fn get_type_alias(ctx: &AssistContext, path: &ast::PathType) -> Option<ast::TypeAlias> {
+    let resolved_path = ctx.sema.resolve_path(&path.path()?)?;
+
+    // We need the generics in the correct order to be able to map any provided
+    // instance generics to declaration generics. The `hir::TypeAlias` doesn't
+    // keep the order, so we must get the `ast::TypeAlias` from the hir
+    // definition.
+    if let PathResolution::Def(hir::ModuleDef::TypeAlias(ta)) = resolved_path {
+        Some(ctx.sema.source(ta)?.value)
+    } else {
+        None
+    }
+}
+
+enum ConstOrTypeGeneric {
+    ConstArg(ast::ConstArg),
+    TypeArg(ast::TypeArg),
+    ConstParam(ast::ConstParam),
+    TypeParam(ast::TypeParam),
+}
+
+impl ConstOrTypeGeneric {
+    fn replacement_key(&self) -> Option<String> {
+        // Only params are used as replacement keys.
+        match self {
+            ConstOrTypeGeneric::ConstParam(cp) => Some(cp.name()?.to_string()),
+            ConstOrTypeGeneric::TypeParam(tp) => Some(tp.name()?.to_string()),
+            _ => None,
+        }
+    }
+
+    fn replacement_value(&self) -> Option<SyntaxNode> {
+        Some(match self {
+            ConstOrTypeGeneric::ConstArg(ca) => ca.expr()?.syntax().clone(),
+            ConstOrTypeGeneric::TypeArg(ta) => ta.syntax().clone(),
+            ConstOrTypeGeneric::ConstParam(cp) => cp.default_val()?.syntax().clone(),
+            ConstOrTypeGeneric::TypeParam(tp) => tp.default_type()?.syntax().clone(),
+        })
+    }
+}
+
+fn generic_param_list_to_const_and_type_generics(
+    generics: &ast::GenericParamList,
+) -> Vec<ConstOrTypeGeneric> {
+    let mut others = Vec::new();
+
+    for param in generics.generic_params() {
+        match param {
+            ast::GenericParam::LifetimeParam(_) => {}
+            ast::GenericParam::ConstParam(cp) => {
+                others.push(ConstOrTypeGeneric::ConstParam(cp));
+            }
+            ast::GenericParam::TypeParam(tp) => others.push(ConstOrTypeGeneric::TypeParam(tp)),
+        }
+    }
+
+    others
+}
+
+fn generic_args_to_const_and_type_generics(
+    generics: &Option<ast::GenericArgList>,
+) -> Vec<ConstOrTypeGeneric> {
+    let mut others = Vec::new();
+
+    // It's fine for there to be no instance generics because the declaration
+    // might have default values or they might be inferred.
+    if let Some(generics) = generics {
+        for arg in generics.generic_args() {
+            match arg {
+                ast::GenericArg::TypeArg(ta) => {
+                    others.push(ConstOrTypeGeneric::TypeArg(ta));
+                }
+                ast::GenericArg::ConstArg(ca) => {
+                    others.push(ConstOrTypeGeneric::ConstArg(ca));
+                }
+                _ => {}
+            }
+        }
+    }
+
+    others
+}
+
+#[cfg(test)]
+mod test {
+    use super::*;
+    use crate::tests::{check_assist, check_assist_not_applicable};
+
+    #[test]
+    fn empty_generic_params() {
+        cov_mark::check!(no_generics_params);
+        check_assist_not_applicable(
+            inline_type_alias,
+            r#"
+type A<> = T;
+fn main() {
+    let a: $0A<u32>;
+}
+            "#,
+        );
+    }
+
+    #[test]
+    fn too_many_generic_args() {
+        cov_mark::check!(too_many_generic_args);
+        check_assist_not_applicable(
+            inline_type_alias,
+            r#"
+type A<T> = T;
+fn main() {
+    let a: $0A<u32, u64>;
+}
+            "#,
+        );
+    }
+
+    #[test]
+    fn too_many_lifetimes() {
+        cov_mark::check!(too_many_lifetimes);
+        check_assist_not_applicable(
+            inline_type_alias,
+            r#"
+type A<'a> = &'a &'b u32;
+fn f<'a>() {
+    let a: $0A<'a, 'b> = 0;
+}
+"#,
+        );
+    }
+
+    // This must be supported in order to support "inline_alias_to_users" or
+    // whatever it will be called.
+    #[test]
+    fn alias_as_expression_ignored() {
+        check_assist_not_applicable(
+            inline_type_alias,
+            r#"
+type A = Vec<u32>;
+fn main() {
+    let a: A = $0A::new();
+}
+"#,
+        );
+    }
+
+    #[test]
+    fn primitive_arg() {
+        check_assist(
+            inline_type_alias,
+            r#"
+type A<T> = T;
+fn main() {
+    let a: $0A<u32> = 0;
+}
+"#,
+            r#"
+type A<T> = T;
+fn main() {
+    let a: u32 = 0;
+}
+"#,
+        );
+    }
+
+    #[test]
+    fn no_generic_replacements() {
+        check_assist(
+            inline_type_alias,
+            r#"
+type A = Vec<u32>;
+fn main() {
+    let a: $0A;
+}
+"#,
+            r#"
+type A = Vec<u32>;
+fn main() {
+    let a: Vec<u32>;
+}
+"#,
+        );
+    }
+
+    #[test]
+    fn param_expression() {
+        check_assist(
+            inline_type_alias,
+            r#"
+type A<const N: usize = { 1 }> = [u32; N];
+fn main() {
+    let a: $0A;
+}
+"#,
+            r#"
+type A<const N: usize = { 1 }> = [u32; N];
+fn main() {
+    let a: [u32; { 1 }];
+}
+"#,
+        );
+    }
+
+    #[test]
+    fn param_default_value() {
+        check_assist(
+            inline_type_alias,
+            r#"
+type A<const N: usize = 1> = [u32; N];
+fn main() {
+    let a: $0A;
+}
+"#,
+            r#"
+type A<const N: usize = 1> = [u32; N];
+fn main() {
+    let a: [u32; 1];
+}
+"#,
+        );
+    }
+
+    #[test]
+    fn all_param_types() {
+        check_assist(
+            inline_type_alias,
+            r#"
+struct Struct<const C: usize>;
+type A<'inner1, 'outer1, Outer1, const INNER1: usize, Inner1: Clone, const OUTER1: usize> = (Struct<INNER1>, Struct<OUTER1>, Outer1, &'inner1 (), Inner1, &'outer1 ());
+fn foo<'inner2, 'outer2, Outer2, const INNER2: usize, Inner2, const OUTER2: usize>() {
+    let a: $0A<'inner2, 'outer2, Outer2, INNER2, Inner2, OUTER2>;
+}
+"#,
+            r#"
+struct Struct<const C: usize>;
+type A<'inner1, 'outer1, Outer1, const INNER1: usize, Inner1: Clone, const OUTER1: usize> = (Struct<INNER1>, Struct<OUTER1>, Outer1, &'inner1 (), Inner1, &'outer1 ());
+fn foo<'inner2, 'outer2, Outer2, const INNER2: usize, Inner2, const OUTER2: usize>() {
+    let a: (Struct<INNER2>, Struct<OUTER2>, Outer2, &'inner2 (), Inner2, &'outer2 ());
+}
+"#,
+        );
+    }
+
+    #[test]
+    fn omitted_lifetimes() {
+        check_assist(
+            inline_type_alias,
+            r#"
+type A<'l, 'r> = &'l &'r u32;
+fn main() {
+    let a: $0A;
+}
+"#,
+            r#"
+type A<'l, 'r> = &'l &'r u32;
+fn main() {
+    let a: &&u32;
+}
+"#,
+        );
+    }
+
+    #[test]
+    fn omitted_type() {
+        check_assist(
+            inline_type_alias,
+            r#"
+type A<'r, 'l, T = u32> = &'l std::collections::HashMap<&'r str, T>;
+fn main() {
+    let a: $0A<'_, '_>;
+}
+"#,
+            r#"
+type A<'r, 'l, T = u32> = &'l std::collections::HashMap<&'r str, T>;
+fn main() {
+    let a: &std::collections::HashMap<&str, u32>;
+}
+"#,
+        );
+    }
+
+    #[test]
+    fn omitted_everything() {
+        check_assist(
+            inline_type_alias,
+            r#"
+type A<'r, 'l, T = u32> = &'l std::collections::HashMap<&'r str, T>;
+fn main() {
+    let v = std::collections::HashMap<&str, u32>;
+    let a: $0A = &v;
+}
+"#,
+            r#"
+type A<'r, 'l, T = u32> = &'l std::collections::HashMap<&'r str, T>;
+fn main() {
+    let v = std::collections::HashMap<&str, u32>;
+    let a: &std::collections::HashMap<&str, u32> = &v;
+}
+"#,
+        );
+    }
+
+    // This doesn't actually cause the GenericArgsList to contain a AssocTypeArg.
+    #[test]
+    fn arg_associated_type() {
+        check_assist(
+            inline_type_alias,
+            r#"
+trait Tra { type Assoc; fn a(); }
+struct Str {}
+impl Tra for Str {
+    type Assoc = u32;
+    fn a() {
+        type A<T> = Vec<T>;
+        let a: $0A<Self::Assoc>;
+    }
+}
+"#,
+            r#"
+trait Tra { type Assoc; fn a(); }
+struct Str {}
+impl Tra for Str {
+    type Assoc = u32;
+    fn a() {
+        type A<T> = Vec<T>;
+        let a: Vec<Self::Assoc>;
+    }
+}
+"#,
+        );
+    }
+
+    #[test]
+    fn param_default_associated_type() {
+        check_assist(
+            inline_type_alias,
+            r#"
+trait Tra { type Assoc; fn a() }
+struct Str {}
+impl Tra for Str {
+    type Assoc = u32;
+    fn a() {
+        type A<T = Self::Assoc> = Vec<T>;
+        let a: $0A;
+    }
+}
+"#,
+            r#"
+trait Tra { type Assoc; fn a() }
+struct Str {}
+impl Tra for Str {
+    type Assoc = u32;
+    fn a() {
+        type A<T = Self::Assoc> = Vec<T>;
+        let a: Vec<Self::Assoc>;
+    }
+}
+"#,
+        );
+    }
+
+    #[test]
+    fn function_pointer() {
+        check_assist(
+            inline_type_alias,
+            r#"
+type A = fn(u32);
+fn foo(a: u32) {}
+fn main() {
+    let a: $0A = foo;
+}
+"#,
+            r#"
+type A = fn(u32);
+fn foo(a: u32) {}
+fn main() {
+    let a: fn(u32) = foo;
+}
+"#,
+        );
+    }
+
+    #[test]
+    fn closure() {
+        check_assist(
+            inline_type_alias,
+            r#"
+type A = Box<dyn FnOnce(u32) -> u32>;
+fn main() {
+    let a: $0A = Box::new(|_| 0);
+}
+"#,
+            r#"
+type A = Box<dyn FnOnce(u32) -> u32>;
+fn main() {
+    let a: Box<dyn FnOnce(u32) -> u32> = Box::new(|_| 0);
+}
+"#,
+        );
+    }
+
+    // Type aliases can't be used in traits, but someone might use the assist to
+    // fix the error.
+    #[test]
+    fn bounds() {
+        check_assist(
+            inline_type_alias,
+            r#"type A = std::io::Write; fn f<T>() where T: $0A {}"#,
+            r#"type A = std::io::Write; fn f<T>() where T: std::io::Write {}"#,
+        );
+    }
+
+    #[test]
+    fn function_parameter() {
+        check_assist(
+            inline_type_alias,
+            r#"
+type A = std::io::Write;
+fn f(a: impl $0A) {}
+"#,
+            r#"
+type A = std::io::Write;
+fn f(a: impl std::io::Write) {}
+"#,
+        );
+    }
+
+    #[test]
+    fn arg_expression() {
+        check_assist(
+            inline_type_alias,
+            r#"
+type A<const N: usize> = [u32; N];
+fn main() {
+    let a: $0A<{ 1 + 1 }>;
+}
+"#,
+            r#"
+type A<const N: usize> = [u32; N];
+fn main() {
+    let a: [u32; { 1 + 1 }];
+}
+"#,
+        )
+    }
+
+    #[test]
+    fn alias_instance_generic_path() {
+        check_assist(
+            inline_type_alias,
+            r#"
+type A<const N: usize> = [u32; N];
+fn main() {
+    let a: $0A<u32::MAX>;
+}
+"#,
+            r#"
+type A<const N: usize> = [u32; N];
+fn main() {
+    let a: [u32; u32::MAX];
+}
+"#,
+        )
+    }
+
+    #[test]
+    fn generic_type() {
+        check_assist(
+            inline_type_alias,
+            r#"
+type A = String;
+fn f(a: Vec<$0A>) {}
+"#,
+            r#"
+type A = String;
+fn f(a: Vec<String>) {}
+"#,
+        );
+    }
+
+    #[test]
+    fn missing_replacement_param() {
+        cov_mark::check!(missing_replacement_param);
+        check_assist_not_applicable(
+            inline_type_alias,
+            r#"
+type A<U> = Vec<T>;
+fn main() {
+    let a: $0A;
+}
+"#,
+        );
+    }
+
+    #[test]
+    fn full_path_type_is_replaced() {
+        check_assist(
+            inline_type_alias,
+            r#"
+mod foo {
+    pub type A = String;
+}
+fn main() {
+    let a: foo::$0A;
+}
+"#,
+            r#"
+mod foo {
+    pub type A = String;
+}
+fn main() {
+    let a: String;
+}
+"#,
+        );
+    }
+}
diff --git a/crates/ide_assists/src/lib.rs b/crates/ide_assists/src/lib.rs
index 067f4d8e14d..6eff8871e8a 100644
--- a/crates/ide_assists/src/lib.rs
+++ b/crates/ide_assists/src/lib.rs
@@ -42,7 +42,7 @@
 //!   useful and (worse) less predictable. The user should have a clear
 //!   intuition when each particular assist is available.
 //! * Make small assists, which compose. Example: rather than auto-importing
-//!   enums in `fill_match_arms`, we use fully-qualified names. There's a
+//!   enums in `add_missing_match_arms`, we use fully-qualified names. There's a
 //!   separate assist to shorten a fully-qualified name.
 //! * Distinguish between assists and fixits for diagnostics. Internally, fixits
 //!   and assists are equivalent. They have the same "show a list + invoke a
@@ -150,6 +150,7 @@ mod handlers {
     mod add_return_type;
     mod inline_call;
     mod inline_local_variable;
+    mod inline_type_alias;
     mod introduce_named_lifetime;
     mod invert_if;
     mod merge_imports;
@@ -231,6 +232,7 @@ mod handlers {
             inline_call::inline_call,
             inline_call::inline_into_callers,
             inline_local_variable::inline_local_variable,
+            inline_type_alias::inline_type_alias,
             introduce_named_generic::introduce_named_generic,
             introduce_named_lifetime::introduce_named_lifetime,
             invert_if::invert_if,
diff --git a/crates/ide_assists/src/tests/generated.rs b/crates/ide_assists/src/tests/generated.rs
index bf69a94c271..3d0e134a9bb 100644
--- a/crates/ide_assists/src/tests/generated.rs
+++ b/crates/ide_assists/src/tests/generated.rs
@@ -1247,6 +1247,27 @@ fn main() {
 }
 
 #[test]
+fn doctest_inline_type_alias() {
+    check_doc_test(
+        "inline_type_alias",
+        r#####"
+type A<T = u32> = Vec<T>;
+
+fn main() {
+    let a: $0A;
+}
+"#####,
+        r#####"
+type A<T = u32> = Vec<T>;
+
+fn main() {
+    let a: Vec<u32>;
+}
+"#####,
+    )
+}
+
+#[test]
 fn doctest_introduce_named_generic() {
     check_doc_test(
         "introduce_named_generic",
diff --git a/crates/syntax/src/ast/node_ext.rs b/crates/syntax/src/ast/node_ext.rs
index 333ee35d636..e1d4addb52f 100644
--- a/crates/syntax/src/ast/node_ext.rs
+++ b/crates/syntax/src/ast/node_ext.rs
@@ -764,6 +764,15 @@ impl ast::Meta {
     }
 }
 
+impl ast::GenericArgList {
+    pub fn lifetime_args(&self) -> impl Iterator<Item = ast::LifetimeArg> {
+        self.generic_args().filter_map(|arg| match arg {
+            ast::GenericArg::LifetimeArg(it) => Some(it),
+            _ => None,
+        })
+    }
+}
+
 impl ast::GenericParamList {
     pub fn lifetime_params(&self) -> impl Iterator<Item = ast::LifetimeParam> {
         self.generic_params().filter_map(|param| match param {