1#[cfg(test)]
3mod tests;
4
5use std::cmp::Ordering;
6
7use hir::Semantics;
8use syntax::{
9 Direction, NodeOrToken, SyntaxKind, SyntaxNode, algo,
10 ast::{
11 self, AstNode, HasAttrs, HasModuleItem, HasVisibility, PathSegmentKind,
12 edit_in_place::Removable, make, syntax_factory::SyntaxFactory,
13 },
14 syntax_editor::{Position, SyntaxEditor},
15 ted,
16};
17
18use crate::{
19 RootDatabase,
20 imports::merge_imports::{
21 MergeBehavior, NormalizationStyle, common_prefix, eq_attrs, eq_visibility,
22 try_merge_imports, use_tree_cmp,
23 },
24};
25
26pub use hir::PrefixKind;
27
28#[derive(Copy, Clone, Debug, PartialEq, Eq)]
30pub enum ImportGranularity {
31 Crate,
33 Module,
35 Item,
37 One,
40}
41
42impl From<ImportGranularity> for NormalizationStyle {
43 fn from(granularity: ImportGranularity) -> Self {
44 match granularity {
45 ImportGranularity::One => NormalizationStyle::One,
46 _ => NormalizationStyle::Default,
47 }
48 }
49}
50
51#[derive(Clone, Copy, Debug, PartialEq, Eq)]
52pub struct InsertUseConfig {
53 pub granularity: ImportGranularity,
54 pub enforce_granularity: bool,
55 pub prefix_kind: PrefixKind,
56 pub group: bool,
57 pub skip_glob_imports: bool,
58}
59
60#[derive(Debug, Clone)]
61pub struct ImportScope {
62 pub kind: ImportScopeKind,
63 pub required_cfgs: Vec<ast::Attr>,
64}
65
66#[derive(Debug, Clone)]
67pub enum ImportScopeKind {
68 File(ast::SourceFile),
69 Module(ast::ItemList),
70 Block(ast::StmtList),
71}
72
73impl ImportScope {
74 pub fn find_insert_use_container(
77 position: &SyntaxNode,
78 sema: &Semantics<'_, RootDatabase>,
79 ) -> Option<Self> {
80 let mut block = None;
82 let mut required_cfgs = Vec::new();
83 for syntax in sema.ancestors_with_macros(position.clone()) {
87 if let Some(file) = ast::SourceFile::cast(syntax.clone()) {
88 return Some(ImportScope { kind: ImportScopeKind::File(file), required_cfgs });
89 } else if let Some(module) = ast::Module::cast(syntax.clone()) {
90 return sema
93 .original_ast_node(module)?
94 .item_list()
95 .map(ImportScopeKind::Module)
96 .map(|kind| ImportScope { kind, required_cfgs });
97 } else if let Some(has_attrs) = ast::AnyHasAttrs::cast(syntax) {
98 if block.is_none()
99 && let Some(b) = ast::BlockExpr::cast(has_attrs.syntax().clone())
100 && let Some(b) = sema.original_ast_node(b)
101 {
102 block = b.stmt_list();
103 }
104 if has_attrs
105 .attrs()
106 .any(|attr| attr.as_simple_call().is_some_and(|(ident, _)| ident == "cfg"))
107 {
108 if let Some(b) = block {
109 return Some(ImportScope {
110 kind: ImportScopeKind::Block(b),
111 required_cfgs,
112 });
113 }
114 required_cfgs.extend(has_attrs.attrs().filter(|attr| {
115 attr.as_simple_call().is_some_and(|(ident, _)| ident == "cfg")
116 }));
117 }
118 }
119 }
120 None
121 }
122
123 pub fn as_syntax_node(&self) -> &SyntaxNode {
124 match &self.kind {
125 ImportScopeKind::File(file) => file.syntax(),
126 ImportScopeKind::Module(item_list) => item_list.syntax(),
127 ImportScopeKind::Block(block) => block.syntax(),
128 }
129 }
130
131 pub fn clone_for_update(&self) -> Self {
132 Self {
133 kind: match &self.kind {
134 ImportScopeKind::File(file) => ImportScopeKind::File(file.clone_for_update()),
135 ImportScopeKind::Module(item_list) => {
136 ImportScopeKind::Module(item_list.clone_for_update())
137 }
138 ImportScopeKind::Block(block) => ImportScopeKind::Block(block.clone_for_update()),
139 },
140 required_cfgs: self.required_cfgs.iter().map(|attr| attr.clone_for_update()).collect(),
141 }
142 }
143}
144
145pub fn insert_use(scope: &ImportScope, path: ast::Path, cfg: &InsertUseConfig) {
147 insert_use_with_alias_option(scope, path, cfg, None);
148}
149
150pub fn insert_use_with_editor(
152 scope: &ImportScope,
153 path: ast::Path,
154 cfg: &InsertUseConfig,
155 syntax_editor: &mut SyntaxEditor,
156 syntax_factory: &SyntaxFactory,
157) {
158 insert_use_with_alias_option_with_editor(scope, path, cfg, None, syntax_editor, syntax_factory);
159}
160
161pub fn insert_use_as_alias(
162 scope: &ImportScope,
163 path: ast::Path,
164 cfg: &InsertUseConfig,
165 edition: span::Edition,
166) {
167 let text: &str = "use foo as _";
168 let parse = syntax::SourceFile::parse(text, edition);
169 let node = parse
170 .tree()
171 .syntax()
172 .descendants()
173 .find_map(ast::UseTree::cast)
174 .expect("Failed to make ast node `Rename`");
175 let alias = node.rename();
176
177 insert_use_with_alias_option(scope, path, cfg, alias);
178}
179
180fn insert_use_with_alias_option(
181 scope: &ImportScope,
182 path: ast::Path,
183 cfg: &InsertUseConfig,
184 alias: Option<ast::Rename>,
185) {
186 let _p = tracing::info_span!("insert_use_with_alias_option").entered();
187 let mut mb = match cfg.granularity {
188 ImportGranularity::Crate => Some(MergeBehavior::Crate),
189 ImportGranularity::Module => Some(MergeBehavior::Module),
190 ImportGranularity::One => Some(MergeBehavior::One),
191 ImportGranularity::Item => None,
192 };
193 if !cfg.enforce_granularity {
194 let file_granularity = guess_granularity_from_scope(scope);
195 mb = match file_granularity {
196 ImportGranularityGuess::Unknown => mb,
197 ImportGranularityGuess::Item => None,
198 ImportGranularityGuess::Module => Some(MergeBehavior::Module),
199 ImportGranularityGuess::ModuleOrItem => match mb {
201 Some(MergeBehavior::Module) | None => mb,
202 Some(MergeBehavior::One | MergeBehavior::Crate) => Some(MergeBehavior::Module),
205 },
206 ImportGranularityGuess::Crate => Some(MergeBehavior::Crate),
207 ImportGranularityGuess::CrateOrModule => match mb {
208 Some(MergeBehavior::Crate | MergeBehavior::Module) => mb,
209 Some(MergeBehavior::One) | None => Some(MergeBehavior::Crate),
210 },
211 ImportGranularityGuess::One => Some(MergeBehavior::One),
212 };
213 }
214
215 let mut use_tree = make::use_tree(path, None, alias, false);
216 if mb == Some(MergeBehavior::One) && use_tree.path().is_some() {
217 use_tree = use_tree.clone_for_update();
218 use_tree.wrap_in_tree_list();
219 }
220 let use_item = make::use_(None, None, use_tree).clone_for_update();
221 for attr in
222 scope.required_cfgs.iter().map(|attr| attr.syntax().clone_subtree().clone_for_update())
223 {
224 ted::insert(ted::Position::first_child_of(use_item.syntax()), attr);
225 }
226
227 if let Some(mb) = mb {
229 let filter = |it: &_| !(cfg.skip_glob_imports && ast::Use::is_simple_glob(it));
230 for existing_use in
231 scope.as_syntax_node().children().filter_map(ast::Use::cast).filter(filter)
232 {
233 if let Some(merged) = try_merge_imports(&existing_use, &use_item, mb) {
234 ted::replace(existing_use.syntax(), merged.syntax());
235 return;
236 }
237 }
238 }
239 insert_use_(scope, use_item, cfg.group);
242}
243
244fn insert_use_with_alias_option_with_editor(
245 scope: &ImportScope,
246 path: ast::Path,
247 cfg: &InsertUseConfig,
248 alias: Option<ast::Rename>,
249 syntax_editor: &mut SyntaxEditor,
250 syntax_factory: &SyntaxFactory,
251) {
252 let _p = tracing::info_span!("insert_use_with_alias_option").entered();
253 let mut mb = match cfg.granularity {
254 ImportGranularity::Crate => Some(MergeBehavior::Crate),
255 ImportGranularity::Module => Some(MergeBehavior::Module),
256 ImportGranularity::One => Some(MergeBehavior::One),
257 ImportGranularity::Item => None,
258 };
259 if !cfg.enforce_granularity {
260 let file_granularity = guess_granularity_from_scope(scope);
261 mb = match file_granularity {
262 ImportGranularityGuess::Unknown => mb,
263 ImportGranularityGuess::Item => None,
264 ImportGranularityGuess::Module => Some(MergeBehavior::Module),
265 ImportGranularityGuess::ModuleOrItem => match mb {
267 Some(MergeBehavior::Module) | None => mb,
268 Some(MergeBehavior::One | MergeBehavior::Crate) => Some(MergeBehavior::Module),
271 },
272 ImportGranularityGuess::Crate => Some(MergeBehavior::Crate),
273 ImportGranularityGuess::CrateOrModule => match mb {
274 Some(MergeBehavior::Crate | MergeBehavior::Module) => mb,
275 Some(MergeBehavior::One) | None => Some(MergeBehavior::Crate),
276 },
277 ImportGranularityGuess::One => Some(MergeBehavior::One),
278 };
279 }
280
281 let use_tree = syntax_factory.use_tree(path, None, alias, false);
282 if mb == Some(MergeBehavior::One) && use_tree.path().is_some() {
283 use_tree.wrap_in_tree_list();
284 }
285 let use_item = make::use_(None, None, use_tree).clone_for_update();
286 for attr in
287 scope.required_cfgs.iter().map(|attr| attr.syntax().clone_subtree().clone_for_update())
288 {
289 syntax_editor.insert(Position::first_child_of(use_item.syntax()), attr);
290 }
291
292 if let Some(mb) = mb {
294 let filter = |it: &_| !(cfg.skip_glob_imports && ast::Use::is_simple_glob(it));
295 for existing_use in
296 scope.as_syntax_node().children().filter_map(ast::Use::cast).filter(filter)
297 {
298 if let Some(merged) = try_merge_imports(&existing_use, &use_item, mb) {
299 syntax_editor.replace(existing_use.syntax(), merged.syntax());
300 return;
301 }
302 }
303 }
304 insert_use_with_editor_(scope, use_item, cfg.group, syntax_editor, syntax_factory);
307}
308
309pub fn ast_to_remove_for_path_in_use_stmt(path: &ast::Path) -> Option<Box<dyn Removable>> {
310 if path.parent_path().is_some() {
312 return None;
313 }
314 let use_tree = path.syntax().parent().and_then(ast::UseTree::cast)?;
315 if use_tree.use_tree_list().is_some() || use_tree.star_token().is_some() {
316 return None;
317 }
318 if let Some(use_) = use_tree.syntax().parent().and_then(ast::Use::cast) {
319 return Some(Box::new(use_));
320 }
321 Some(Box::new(use_tree))
322}
323
324pub fn remove_path_if_in_use_stmt(path: &ast::Path) {
325 if let Some(node) = ast_to_remove_for_path_in_use_stmt(path) {
326 node.remove();
327 }
328}
329
330#[derive(Eq, PartialEq, PartialOrd, Ord)]
331enum ImportGroup {
332 Std,
334 ExternCrate,
335 ThisCrate,
336 ThisModule,
337 SuperModule,
338 One,
339}
340
341impl ImportGroup {
342 fn new(use_tree: &ast::UseTree) -> ImportGroup {
343 if use_tree.path().is_none() && use_tree.use_tree_list().is_some() {
344 return ImportGroup::One;
345 }
346
347 let Some(first_segment) = use_tree.path().as_ref().and_then(ast::Path::first_segment)
348 else {
349 return ImportGroup::ExternCrate;
350 };
351
352 let kind = first_segment.kind().unwrap_or(PathSegmentKind::SelfKw);
353 match kind {
354 PathSegmentKind::SelfKw => ImportGroup::ThisModule,
355 PathSegmentKind::SuperKw => ImportGroup::SuperModule,
356 PathSegmentKind::CrateKw => ImportGroup::ThisCrate,
357 PathSegmentKind::Name(name) => match name.text().as_str() {
358 "std" => ImportGroup::Std,
359 "core" => ImportGroup::Std,
360 _ => ImportGroup::ExternCrate,
361 },
362 PathSegmentKind::SelfTypeKw => ImportGroup::ExternCrate,
364 PathSegmentKind::Type { .. } => ImportGroup::ExternCrate,
365 }
366 }
367}
368
369#[derive(PartialEq, PartialOrd, Debug, Clone, Copy)]
370enum ImportGranularityGuess {
371 Unknown,
372 Item,
373 Module,
374 ModuleOrItem,
375 Crate,
376 CrateOrModule,
377 One,
378}
379
380fn guess_granularity_from_scope(scope: &ImportScope) -> ImportGranularityGuess {
381 let use_stmt = |item| match item {
384 ast::Item::Use(use_) => {
385 let use_tree = use_.use_tree()?;
386 Some((use_tree, use_.visibility(), use_.attrs()))
387 }
388 _ => None,
389 };
390 let mut use_stmts = match &scope.kind {
391 ImportScopeKind::File(f) => f.items(),
392 ImportScopeKind::Module(m) => m.items(),
393 ImportScopeKind::Block(b) => b.items(),
394 }
395 .filter_map(use_stmt);
396 let mut res = ImportGranularityGuess::Unknown;
397 let Some((mut prev, mut prev_vis, mut prev_attrs)) = use_stmts.next() else { return res };
398
399 let is_tree_one_style =
400 |use_tree: &ast::UseTree| use_tree.path().is_none() && use_tree.use_tree_list().is_some();
401 let mut seen_one_style_groups = Vec::new();
402
403 loop {
404 if is_tree_one_style(&prev) {
405 if res != ImportGranularityGuess::One {
406 if res != ImportGranularityGuess::Unknown {
407 break ImportGranularityGuess::Unknown;
409 }
410
411 res = ImportGranularityGuess::One;
412 seen_one_style_groups.push((prev_vis.clone(), prev_attrs.clone()));
413 }
414 } else if let Some(use_tree_list) = prev.use_tree_list() {
415 if use_tree_list.use_trees().any(|tree| tree.use_tree_list().is_some()) {
416 break ImportGranularityGuess::Crate;
418 } else {
419 res = ImportGranularityGuess::CrateOrModule;
421 }
422 }
423
424 let Some((curr, curr_vis, curr_attrs)) = use_stmts.next() else { break res };
425 if is_tree_one_style(&curr) {
426 if res != ImportGranularityGuess::One
427 || seen_one_style_groups.iter().any(|(prev_vis, prev_attrs)| {
428 eq_visibility(prev_vis.clone(), curr_vis.clone())
429 && eq_attrs(prev_attrs.clone(), curr_attrs.clone())
430 })
431 {
432 break ImportGranularityGuess::Unknown;
435 }
436 seen_one_style_groups.push((curr_vis.clone(), curr_attrs.clone()));
437 } else if eq_visibility(prev_vis, curr_vis.clone())
438 && eq_attrs(prev_attrs, curr_attrs.clone())
439 && let Some((prev_path, curr_path)) = prev.path().zip(curr.path())
440 && let Some((prev_prefix, _)) = common_prefix(&prev_path, &curr_path)
441 {
442 if prev.use_tree_list().is_none() && curr.use_tree_list().is_none() {
443 let prefix_c = prev_prefix.qualifiers().count();
444 let curr_c = curr_path.qualifiers().count() - prefix_c;
445 let prev_c = prev_path.qualifiers().count() - prefix_c;
446 if curr_c == 1 && prev_c == 1 {
447 break ImportGranularityGuess::Item;
449 } else {
450 res = ImportGranularityGuess::ModuleOrItem;
452 }
453 } else {
454 break ImportGranularityGuess::Module;
457 }
458 }
459 prev = curr;
460 prev_vis = curr_vis;
461 prev_attrs = curr_attrs;
462 }
463}
464
465fn insert_use_(scope: &ImportScope, use_item: ast::Use, group_imports: bool) {
466 let scope_syntax = scope.as_syntax_node();
467 let insert_use_tree =
468 use_item.use_tree().expect("`use_item` should have a use tree for `insert_path`");
469 let group = ImportGroup::new(&insert_use_tree);
470 let path_node_iter = scope_syntax
471 .children()
472 .filter_map(|node| ast::Use::cast(node.clone()).zip(Some(node)))
473 .flat_map(|(use_, node)| {
474 let tree = use_.use_tree()?;
475 Some((tree, node))
476 });
477
478 if group_imports {
479 let group_iter = path_node_iter
482 .clone()
483 .skip_while(|(use_tree, ..)| ImportGroup::new(use_tree) != group)
484 .take_while(|(use_tree, ..)| ImportGroup::new(use_tree) == group);
485
486 let mut last = None;
488 let post_insert: Option<(_, SyntaxNode)> = group_iter
490 .inspect(|(.., node)| last = Some(node.clone()))
491 .find(|(use_tree, _)| use_tree_cmp(&insert_use_tree, use_tree) != Ordering::Greater);
492
493 if let Some((.., node)) = post_insert {
494 cov_mark::hit!(insert_group);
495 return ted::insert(ted::Position::before(node), use_item.syntax());
497 }
498 if let Some(node) = last {
499 cov_mark::hit!(insert_group_last);
500 return ted::insert(ted::Position::after(node), use_item.syntax());
502 }
503
504 let mut last = None;
507 let post_group = path_node_iter
509 .inspect(|(.., node)| last = Some(node.clone()))
510 .find(|(use_tree, ..)| ImportGroup::new(use_tree) > group);
511 if let Some((.., node)) = post_group {
512 cov_mark::hit!(insert_group_new_group);
513 ted::insert(ted::Position::before(&node), use_item.syntax());
514 if let Some(node) = algo::non_trivia_sibling(node.into(), Direction::Prev) {
515 ted::insert(ted::Position::after(node), make::tokens::single_newline());
516 }
517 return;
518 }
519 if let Some(node) = last {
521 cov_mark::hit!(insert_group_no_group);
522 ted::insert(ted::Position::after(&node), use_item.syntax());
523 ted::insert(ted::Position::after(node), make::tokens::single_newline());
524 return;
525 }
526 } else {
527 if let Some((_, node)) = path_node_iter.last() {
529 cov_mark::hit!(insert_no_grouping_last);
530 ted::insert(ted::Position::after(node), use_item.syntax());
531 return;
532 }
533 }
534
535 let l_curly = match &scope.kind {
536 ImportScopeKind::File(_) => None,
537 ImportScopeKind::Module(item_list) => item_list.l_curly_token(),
539 ImportScopeKind::Block(block) => block.l_curly_token(),
541 };
542 if let Some(last_inner_element) = scope_syntax
545 .children_with_tokens()
546 .skip(l_curly.is_some() as usize)
548 .take_while(|child| match child {
549 NodeOrToken::Node(node) => is_inner_attribute(node.clone()),
550 NodeOrToken::Token(token) => {
551 [SyntaxKind::WHITESPACE, SyntaxKind::COMMENT, SyntaxKind::SHEBANG]
552 .contains(&token.kind())
553 }
554 })
555 .filter(|child| child.as_token().is_none_or(|t| t.kind() != SyntaxKind::WHITESPACE))
556 .last()
557 {
558 cov_mark::hit!(insert_empty_inner_attr);
559 ted::insert(ted::Position::after(&last_inner_element), use_item.syntax());
560 ted::insert(ted::Position::after(last_inner_element), make::tokens::single_newline());
561 } else {
562 match l_curly {
563 Some(b) => {
564 cov_mark::hit!(insert_empty_module);
565 ted::insert(ted::Position::after(&b), make::tokens::single_newline());
566 ted::insert(ted::Position::after(&b), use_item.syntax());
567 }
568 None => {
569 cov_mark::hit!(insert_empty_file);
570 ted::insert(
571 ted::Position::first_child_of(scope_syntax),
572 make::tokens::blank_line(),
573 );
574 ted::insert(ted::Position::first_child_of(scope_syntax), use_item.syntax());
575 }
576 }
577 }
578}
579
580fn insert_use_with_editor_(
581 scope: &ImportScope,
582 use_item: ast::Use,
583 group_imports: bool,
584 syntax_editor: &mut SyntaxEditor,
585 syntax_factory: &SyntaxFactory,
586) {
587 let scope_syntax = scope.as_syntax_node();
588 let insert_use_tree =
589 use_item.use_tree().expect("`use_item` should have a use tree for `insert_path`");
590 let group = ImportGroup::new(&insert_use_tree);
591 let path_node_iter = scope_syntax
592 .children()
593 .filter_map(|node| ast::Use::cast(node.clone()).zip(Some(node)))
594 .flat_map(|(use_, node)| {
595 let tree = use_.use_tree()?;
596 Some((tree, node))
597 });
598
599 if group_imports {
600 let group_iter = path_node_iter
603 .clone()
604 .skip_while(|(use_tree, ..)| ImportGroup::new(use_tree) != group)
605 .take_while(|(use_tree, ..)| ImportGroup::new(use_tree) == group);
606
607 let mut last = None;
609 let post_insert: Option<(_, SyntaxNode)> = group_iter
611 .inspect(|(.., node)| last = Some(node.clone()))
612 .find(|(use_tree, _)| use_tree_cmp(&insert_use_tree, use_tree) != Ordering::Greater);
613
614 if let Some((.., node)) = post_insert {
615 cov_mark::hit!(insert_group);
616 return syntax_editor.insert(Position::before(node), use_item.syntax());
618 }
619 if let Some(node) = last {
620 cov_mark::hit!(insert_group_last);
621 return syntax_editor.insert(Position::after(node), use_item.syntax());
623 }
624
625 let mut last = None;
628 let post_group = path_node_iter
630 .inspect(|(.., node)| last = Some(node.clone()))
631 .find(|(use_tree, ..)| ImportGroup::new(use_tree) > group);
632 if let Some((.., node)) = post_group {
633 cov_mark::hit!(insert_group_new_group);
634 syntax_editor.insert(Position::before(&node), use_item.syntax());
635 if let Some(node) = algo::non_trivia_sibling(node.into(), Direction::Prev) {
636 syntax_editor.insert(Position::after(node), syntax_factory.whitespace("\n"));
637 }
638 return;
639 }
640 if let Some(node) = last {
642 cov_mark::hit!(insert_group_no_group);
643 syntax_editor.insert(Position::after(&node), use_item.syntax());
644 syntax_editor.insert(Position::after(node), syntax_factory.whitespace("\n"));
645 return;
646 }
647 } else {
648 if let Some((_, node)) = path_node_iter.last() {
650 cov_mark::hit!(insert_no_grouping_last);
651 syntax_editor.insert(Position::after(node), use_item.syntax());
652 return;
653 }
654 }
655
656 let l_curly = match &scope.kind {
657 ImportScopeKind::File(_) => None,
658 ImportScopeKind::Module(item_list) => item_list.l_curly_token(),
660 ImportScopeKind::Block(block) => block.l_curly_token(),
662 };
663 if let Some(last_inner_element) = scope_syntax
666 .children_with_tokens()
667 .skip(l_curly.is_some() as usize)
669 .take_while(|child| match child {
670 NodeOrToken::Node(node) => is_inner_attribute(node.clone()),
671 NodeOrToken::Token(token) => {
672 [SyntaxKind::WHITESPACE, SyntaxKind::COMMENT, SyntaxKind::SHEBANG]
673 .contains(&token.kind())
674 }
675 })
676 .filter(|child| child.as_token().is_none_or(|t| t.kind() != SyntaxKind::WHITESPACE))
677 .last()
678 {
679 cov_mark::hit!(insert_empty_inner_attr);
680 syntax_editor.insert(Position::after(&last_inner_element), use_item.syntax());
681 syntax_editor.insert(Position::after(last_inner_element), syntax_factory.whitespace("\n"));
682 } else {
683 match l_curly {
684 Some(b) => {
685 cov_mark::hit!(insert_empty_module);
686 syntax_editor.insert(Position::after(&b), syntax_factory.whitespace("\n"));
687 syntax_editor.insert(Position::after(&b), use_item.syntax());
688 }
689 None => {
690 cov_mark::hit!(insert_empty_file);
691 syntax_editor.insert(
692 Position::first_child_of(scope_syntax),
693 syntax_factory.whitespace("\n\n"),
694 );
695 syntax_editor.insert(Position::first_child_of(scope_syntax), use_item.syntax());
696 }
697 }
698 }
699}
700
701fn is_inner_attribute(node: SyntaxNode) -> bool {
702 ast::Attr::cast(node).map(|attr| attr.kind()) == Some(ast::AttrKind::Inner)
703}