Merge remote-tracking branch 'upstream/master' into 503-hover-doc-links

This commit is contained in:
Zac Pullar-Strecker 2020-08-24 21:19:53 +12:00
commit 7bbca7a1b3
1623 changed files with 130619 additions and 128427 deletions

View file

@ -3,3 +3,6 @@ xtask = "run --package xtask --bin xtask --"
install-ra = "run --package xtask --bin xtask -- install" # for backwards compat
tq = "test -- -q"
qt = "tq"
[target.x86_64-pc-windows-msvc]
linker = "rust-lld"

2
.gitattributes vendored
View file

@ -1,5 +1,5 @@
* text=auto eol=lf
crates/ra_syntax/test_data/** -text eof=LF
crates/syntax/test_data/** -text eof=LF
# Older git versions try to fix line endings on images, this prevents it.
*.png binary
*.jpg binary

12
.github/FUNDING.yml vendored
View file

@ -1,12 +1,2 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
github: rust-analyzer
open_collective: rust-analyzer
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

View file

@ -16,20 +16,6 @@ env:
RUSTUP_MAX_RETRIES: 10
jobs:
# rust-audit:
# name: Audit Rust vulnerabilities
# runs-on: ubuntu-latest
# steps:
# - name: Checkout repository
# uses: actions/checkout@v2
# - uses: actions-rs/install@v0.1
# with:
# crate: cargo-audit
# use-tool-cache: true
# - run: cargo audit
rust:
name: Rust
runs-on: ${{ matrix.os }}
@ -84,15 +70,14 @@ jobs:
- name: Prepare cache
run: cargo xtask pre-cache
- name: Prepare cache 2
if: matrix.os == 'windows-latest'
run: Remove-Item ./target/debug/xtask.exe, ./target/debug/deps/xtask.exe
# Weird target to catch non-portable code
rust-power:
name: Rust Power
# Weird targets to catch non-portable code
rust-cross:
name: Rust Cross
runs-on: ubuntu-latest
env:
targets: "powerpc-unknown-linux-gnu x86_64-unknown-linux-musl"
steps:
- name: Checkout repository
uses: actions/checkout@v2
@ -103,7 +88,9 @@ jobs:
toolchain: stable
profile: minimal
override: true
target: 'powerpc-unknown-linux-gnu'
- name: Install Rust targets
run: rustup target add ${{ env.targets }}
- name: Cache cargo directories
uses: actions/cache@v2
@ -114,14 +101,17 @@ jobs:
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Check
run: cargo check --target=powerpc-unknown-linux-gnu --all-targets
run: |
for target in ${{ env.targets }}; do
cargo check --target=$target --all-targets
done
typescript:
name: TypeScript
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}

875
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -2,37 +2,34 @@
members = [ "crates/*", "xtask/" ]
[profile.dev]
# disabling debug info speeds up builds a bunch,
# Disabling debug info speeds up builds a bunch,
# and we don't rely on it for debugging that much.
debug = 0
[profile.dev.package]
# These speed up local tests.
rowan.opt-level = 3
rustc-hash.opt-level = 3
smol_str.opt-level = 3
text-size.opt-level = 3
# This speeds up `cargo xtask dist`.
miniz_oxide.opt-level = 3
[profile.release]
incremental = true
debug = 0 # set this to 1 or 2 to get more useful backtraces in debugger
debug = 0 # Set this to 1 or 2 to get more useful backtraces in debugger.
# ideally, we would use `build-override` here, but some crates are also
# needed at run-time and we end up compiling them twice
[profile.release.package.proc-macro2]
opt-level = 0
[profile.release.package.quote]
opt-level = 0
[profile.release.package.syn]
opt-level = 0
[profile.release.package.serde_derive]
opt-level = 0
[profile.release.package.chalk-derive]
opt-level = 0
[profile.release.package.salsa-macros]
opt-level = 0
[profile.release.package.tracing-attributes]
opt-level = 0
[profile.release.package.xtask]
opt-level = 0
# Gzipping the artifacts is up to 10 times faster with optimizations (`cargo xtask dist`).
# `miniz_oxide` is the direct dependency of `flate2` which does all the heavy lifting
[profile.dev.package.miniz_oxide]
opt-level = 3
# Ideally, we would use `build-override` here, but some crates are also
# needed at run-time and we end up compiling them twice.
[profile.release.package]
chalk-derive.opt-level = 0
proc-macro2.opt-level = 0
quote.opt-level = 0
salsa-macros.opt-level = 0
serde_derive.opt-level = 0
syn.opt-level = 0
tracing-attributes.opt-level = 0
xtask.opt-level = 0
[patch.'crates-io']
# rowan = { path = "../rowan" }

View file

@ -39,7 +39,7 @@ https://rust-lang.zulipchat.com/#narrow/stream/185405-t-compiler.2Frls-2.2E0
* Website: https://rust-analyzer.github.io/
* Metrics: https://rust-analyzer.github.io/metrics/
* API docs: https://rust-analyzer.github.io/rust-analyzer/ra_ide/
* API docs: https://rust-analyzer.github.io/rust-analyzer/ide/
## License

View file

@ -1,9 +1,8 @@
status = [
"Rust (ubuntu-latest)",
"Rust (windows-latest)",
"Rust (macos-latest)",
# "Rust (macos-latest)",
"TypeScript (ubuntu-latest)",
"TypeScript (windows-latest)",
"TypeScript (macos-latest)",
]
delete_merged_branches = true

9
crates/arena/Cargo.toml Normal file
View file

@ -0,0 +1,9 @@
[package]
name = "arena"
version = "0.0.0"
license = "MIT OR Apache-2.0"
authors = ["rust-analyzer developers"]
edition = "2018"
[lib]
doctest = false

23
crates/assists/Cargo.toml Normal file
View file

@ -0,0 +1,23 @@
[package]
name = "assists"
version = "0.0.0"
license = "MIT OR Apache-2.0"
authors = ["rust-analyzer developers"]
edition = "2018"
[lib]
doctest = false
[dependencies]
rustc-hash = "1.1.0"
itertools = "0.9.0"
either = "1.5.3"
stdx = { path = "../stdx" }
syntax = { path = "../syntax" }
text_edit = { path = "../text_edit" }
profile = { path = "../profile" }
base_db = { path = "../base_db" }
ide_db = { path = "../ide_db" }
hir = { path = "../hir" }
test_utils = { path = "../test_utils" }

View file

@ -0,0 +1,293 @@
//! See `AssistContext`
use std::mem;
use algo::find_covering_element;
use base_db::{FileId, FileRange};
use hir::Semantics;
use ide_db::{
label::Label,
source_change::{SourceChange, SourceFileEdit},
RootDatabase,
};
use syntax::{
algo::{self, find_node_at_offset, SyntaxRewriter},
AstNode, SourceFile, SyntaxElement, SyntaxKind, SyntaxToken, TextRange, TextSize,
TokenAtOffset,
};
use text_edit::{TextEdit, TextEditBuilder};
use crate::{
assist_config::{AssistConfig, SnippetCap},
Assist, AssistId, AssistKind, GroupLabel, ResolvedAssist,
};
/// `AssistContext` allows to apply an assist or check if it could be applied.
///
/// Assists use a somewhat over-engineered approach, given the current needs.
/// The assists workflow consists of two phases. In the first phase, a user asks
/// for the list of available assists. In the second phase, the user picks a
/// particular assist and it gets applied.
///
/// There are two peculiarities here:
///
/// * first, we ideally avoid computing more things then necessary to answer "is
/// assist applicable" in the first phase.
/// * second, when we are applying assist, we don't have a guarantee that there
/// weren't any changes between the point when user asked for assists and when
/// they applied a particular assist. So, when applying assist, we need to do
/// all the checks from scratch.
///
/// To avoid repeating the same code twice for both "check" and "apply"
/// functions, we use an approach reminiscent of that of Django's function based
/// views dealing with forms. Each assist receives a runtime parameter,
/// `resolve`. It first check if an edit is applicable (potentially computing
/// info required to compute the actual edit). If it is applicable, and
/// `resolve` is `true`, it then computes the actual edit.
///
/// So, to implement the original assists workflow, we can first apply each edit
/// with `resolve = false`, and then applying the selected edit again, with
/// `resolve = true` this time.
///
/// Note, however, that we don't actually use such two-phase logic at the
/// moment, because the LSP API is pretty awkward in this place, and it's much
/// easier to just compute the edit eagerly :-)
pub(crate) struct AssistContext<'a> {
pub(crate) config: &'a AssistConfig,
pub(crate) sema: Semantics<'a, RootDatabase>,
pub(crate) frange: FileRange,
source_file: SourceFile,
}
impl<'a> AssistContext<'a> {
pub(crate) fn new(
sema: Semantics<'a, RootDatabase>,
config: &'a AssistConfig,
frange: FileRange,
) -> AssistContext<'a> {
let source_file = sema.parse(frange.file_id);
AssistContext { config, sema, frange, source_file }
}
pub(crate) fn db(&self) -> &RootDatabase {
self.sema.db
}
pub(crate) fn source_file(&self) -> &SourceFile {
&self.source_file
}
// NB, this ignores active selection.
pub(crate) fn offset(&self) -> TextSize {
self.frange.range.start()
}
pub(crate) fn token_at_offset(&self) -> TokenAtOffset<SyntaxToken> {
self.source_file.syntax().token_at_offset(self.offset())
}
pub(crate) fn find_token_at_offset(&self, kind: SyntaxKind) -> Option<SyntaxToken> {
self.token_at_offset().find(|it| it.kind() == kind)
}
pub(crate) fn find_node_at_offset<N: AstNode>(&self) -> Option<N> {
find_node_at_offset(self.source_file.syntax(), self.offset())
}
pub(crate) fn find_node_at_offset_with_descend<N: AstNode>(&self) -> Option<N> {
self.sema.find_node_at_offset_with_descend(self.source_file.syntax(), self.offset())
}
pub(crate) fn covering_element(&self) -> SyntaxElement {
find_covering_element(self.source_file.syntax(), self.frange.range)
}
// FIXME: remove
pub(crate) fn covering_node_for_range(&self, range: TextRange) -> SyntaxElement {
find_covering_element(self.source_file.syntax(), range)
}
}
pub(crate) struct Assists {
resolve: bool,
file: FileId,
buf: Vec<(Assist, Option<SourceChange>)>,
allowed: Option<Vec<AssistKind>>,
}
impl Assists {
pub(crate) fn new_resolved(ctx: &AssistContext) -> Assists {
Assists {
resolve: true,
file: ctx.frange.file_id,
buf: Vec::new(),
allowed: ctx.config.allowed.clone(),
}
}
pub(crate) fn new_unresolved(ctx: &AssistContext) -> Assists {
Assists {
resolve: false,
file: ctx.frange.file_id,
buf: Vec::new(),
allowed: ctx.config.allowed.clone(),
}
}
pub(crate) fn finish_unresolved(self) -> Vec<Assist> {
assert!(!self.resolve);
self.finish()
.into_iter()
.map(|(label, edit)| {
assert!(edit.is_none());
label
})
.collect()
}
pub(crate) fn finish_resolved(self) -> Vec<ResolvedAssist> {
assert!(self.resolve);
self.finish()
.into_iter()
.map(|(label, edit)| ResolvedAssist { assist: label, source_change: edit.unwrap() })
.collect()
}
pub(crate) fn add(
&mut self,
id: AssistId,
label: impl Into<String>,
target: TextRange,
f: impl FnOnce(&mut AssistBuilder),
) -> Option<()> {
if !self.is_allowed(&id) {
return None;
}
let label = Label::new(label.into());
let assist = Assist { id, label, group: None, target };
self.add_impl(assist, f)
}
pub(crate) fn add_group(
&mut self,
group: &GroupLabel,
id: AssistId,
label: impl Into<String>,
target: TextRange,
f: impl FnOnce(&mut AssistBuilder),
) -> Option<()> {
if !self.is_allowed(&id) {
return None;
}
let label = Label::new(label.into());
let assist = Assist { id, label, group: Some(group.clone()), target };
self.add_impl(assist, f)
}
fn add_impl(&mut self, assist: Assist, f: impl FnOnce(&mut AssistBuilder)) -> Option<()> {
let source_change = if self.resolve {
let mut builder = AssistBuilder::new(self.file);
f(&mut builder);
Some(builder.finish())
} else {
None
};
self.buf.push((assist, source_change));
Some(())
}
fn finish(mut self) -> Vec<(Assist, Option<SourceChange>)> {
self.buf.sort_by_key(|(label, _edit)| label.target.len());
self.buf
}
fn is_allowed(&self, id: &AssistId) -> bool {
match &self.allowed {
Some(allowed) => allowed.iter().any(|kind| kind.contains(id.1)),
None => true,
}
}
}
pub(crate) struct AssistBuilder {
edit: TextEditBuilder,
file_id: FileId,
is_snippet: bool,
change: SourceChange,
}
impl AssistBuilder {
pub(crate) fn new(file_id: FileId) -> AssistBuilder {
AssistBuilder {
edit: TextEdit::builder(),
file_id,
is_snippet: false,
change: SourceChange::default(),
}
}
pub(crate) fn edit_file(&mut self, file_id: FileId) {
self.file_id = file_id;
}
fn commit(&mut self) {
let edit = mem::take(&mut self.edit).finish();
if !edit.is_empty() {
let new_edit = SourceFileEdit { file_id: self.file_id, edit };
assert!(!self.change.source_file_edits.iter().any(|it| it.file_id == new_edit.file_id));
self.change.source_file_edits.push(new_edit);
}
}
/// Remove specified `range` of text.
pub(crate) fn delete(&mut self, range: TextRange) {
self.edit.delete(range)
}
/// Append specified `text` at the given `offset`
pub(crate) fn insert(&mut self, offset: TextSize, text: impl Into<String>) {
self.edit.insert(offset, text.into())
}
/// Append specified `snippet` at the given `offset`
pub(crate) fn insert_snippet(
&mut self,
_cap: SnippetCap,
offset: TextSize,
snippet: impl Into<String>,
) {
self.is_snippet = true;
self.insert(offset, snippet);
}
/// Replaces specified `range` of text with a given string.
pub(crate) fn replace(&mut self, range: TextRange, replace_with: impl Into<String>) {
self.edit.replace(range, replace_with.into())
}
/// Replaces specified `range` of text with a given `snippet`.
pub(crate) fn replace_snippet(
&mut self,
_cap: SnippetCap,
range: TextRange,
snippet: impl Into<String>,
) {
self.is_snippet = true;
self.replace(range, snippet);
}
pub(crate) fn replace_ast<N: AstNode>(&mut self, old: N, new: N) {
algo::diff(old.syntax(), new.syntax()).into_text_edit(&mut self.edit)
}
pub(crate) fn rewrite(&mut self, rewriter: SyntaxRewriter) {
let node = rewriter.rewrite_root().unwrap();
let new = rewriter.rewrite(&node);
algo::diff(&node, &new).into_text_edit(&mut self.edit);
}
// FIXME: kill this API
/// Get access to the raw `TextEditBuilder`.
pub(crate) fn text_edit_builder(&mut self) -> &mut TextEditBuilder {
&mut self.edit
}
fn finish(mut self) -> SourceChange {
self.commit();
let mut change = mem::take(&mut self.change);
if self.is_snippet {
change.is_snippet = true;
}
change
}
}

View file

@ -0,0 +1,200 @@
//! `AstTransformer`s are functions that replace nodes in an AST and can be easily combined.
use rustc_hash::FxHashMap;
use hir::{HirDisplay, PathResolution, SemanticsScope};
use syntax::{
algo::SyntaxRewriter,
ast::{self, AstNode},
};
pub fn apply<'a, N: AstNode>(transformer: &dyn AstTransform<'a>, node: N) -> N {
SyntaxRewriter::from_fn(|element| match element {
syntax::SyntaxElement::Node(n) => {
let replacement = transformer.get_substitution(&n)?;
Some(replacement.into())
}
_ => None,
})
.rewrite_ast(&node)
}
pub trait AstTransform<'a> {
fn get_substitution(&self, node: &syntax::SyntaxNode) -> Option<syntax::SyntaxNode>;
fn chain_before(self, other: Box<dyn AstTransform<'a> + 'a>) -> Box<dyn AstTransform<'a> + 'a>;
fn or<T: AstTransform<'a> + 'a>(self, other: T) -> Box<dyn AstTransform<'a> + 'a>
where
Self: Sized + 'a,
{
self.chain_before(Box::new(other))
}
}
struct NullTransformer;
impl<'a> AstTransform<'a> for NullTransformer {
fn get_substitution(&self, _node: &syntax::SyntaxNode) -> Option<syntax::SyntaxNode> {
None
}
fn chain_before(self, other: Box<dyn AstTransform<'a> + 'a>) -> Box<dyn AstTransform<'a> + 'a> {
other
}
}
pub struct SubstituteTypeParams<'a> {
source_scope: &'a SemanticsScope<'a>,
substs: FxHashMap<hir::TypeParam, ast::Type>,
previous: Box<dyn AstTransform<'a> + 'a>,
}
impl<'a> SubstituteTypeParams<'a> {
pub fn for_trait_impl(
source_scope: &'a SemanticsScope<'a>,
// FIXME: there's implicit invariant that `trait_` and `source_scope` match...
trait_: hir::Trait,
impl_def: ast::Impl,
) -> SubstituteTypeParams<'a> {
let substs = get_syntactic_substs(impl_def).unwrap_or_default();
let generic_def: hir::GenericDef = trait_.into();
let substs_by_param: FxHashMap<_, _> = generic_def
.params(source_scope.db)
.into_iter()
// this is a trait impl, so we need to skip the first type parameter -- this is a bit hacky
.skip(1)
// The actual list of trait type parameters may be longer than the one
// used in the `impl` block due to trailing default type parameters.
// For that case we extend the `substs` with an empty iterator so we
// can still hit those trailing values and check if they actually have
// a default type. If they do, go for that type from `hir` to `ast` so
// the resulting change can be applied correctly.
.zip(substs.into_iter().map(Some).chain(std::iter::repeat(None)))
.filter_map(|(k, v)| match v {
Some(v) => Some((k, v)),
None => {
let default = k.default(source_scope.db)?;
Some((
k,
ast::make::ty(
&default
.display_source_code(source_scope.db, source_scope.module()?.into())
.ok()?,
),
))
}
})
.collect();
return SubstituteTypeParams {
source_scope,
substs: substs_by_param,
previous: Box::new(NullTransformer),
};
// FIXME: It would probably be nicer if we could get this via HIR (i.e. get the
// trait ref, and then go from the types in the substs back to the syntax).
fn get_syntactic_substs(impl_def: ast::Impl) -> Option<Vec<ast::Type>> {
let target_trait = impl_def.trait_()?;
let path_type = match target_trait {
ast::Type::PathType(path) => path,
_ => return None,
};
let generic_arg_list = path_type.path()?.segment()?.generic_arg_list()?;
let mut result = Vec::new();
for generic_arg in generic_arg_list.generic_args() {
match generic_arg {
ast::GenericArg::TypeArg(type_arg) => result.push(type_arg.ty()?),
ast::GenericArg::AssocTypeArg(_)
| ast::GenericArg::LifetimeArg(_)
| ast::GenericArg::ConstArg(_) => (),
}
}
Some(result)
}
}
fn get_substitution_inner(&self, node: &syntax::SyntaxNode) -> Option<syntax::SyntaxNode> {
let type_ref = ast::Type::cast(node.clone())?;
let path = match &type_ref {
ast::Type::PathType(path_type) => path_type.path()?,
_ => return None,
};
let resolution = self.source_scope.speculative_resolve(&path)?;
match resolution {
hir::PathResolution::TypeParam(tp) => Some(self.substs.get(&tp)?.syntax().clone()),
_ => None,
}
}
}
impl<'a> AstTransform<'a> for SubstituteTypeParams<'a> {
fn get_substitution(&self, node: &syntax::SyntaxNode) -> Option<syntax::SyntaxNode> {
self.get_substitution_inner(node).or_else(|| self.previous.get_substitution(node))
}
fn chain_before(self, other: Box<dyn AstTransform<'a> + 'a>) -> Box<dyn AstTransform<'a> + 'a> {
Box::new(SubstituteTypeParams { previous: other, ..self })
}
}
pub struct QualifyPaths<'a> {
target_scope: &'a SemanticsScope<'a>,
source_scope: &'a SemanticsScope<'a>,
previous: Box<dyn AstTransform<'a> + 'a>,
}
impl<'a> QualifyPaths<'a> {
pub fn new(target_scope: &'a SemanticsScope<'a>, source_scope: &'a SemanticsScope<'a>) -> Self {
Self { target_scope, source_scope, previous: Box::new(NullTransformer) }
}
fn get_substitution_inner(&self, node: &syntax::SyntaxNode) -> Option<syntax::SyntaxNode> {
// FIXME handle value ns?
let from = self.target_scope.module()?;
let p = ast::Path::cast(node.clone())?;
if p.segment().and_then(|s| s.param_list()).is_some() {
// don't try to qualify `Fn(Foo) -> Bar` paths, they are in prelude anyway
return None;
}
let resolution = self.source_scope.speculative_resolve(&p)?;
match resolution {
PathResolution::Def(def) => {
let found_path = from.find_use_path(self.source_scope.db.upcast(), def)?;
let mut path = path_to_ast(found_path);
let type_args = p
.segment()
.and_then(|s| s.generic_arg_list())
.map(|arg_list| apply(self, arg_list));
if let Some(type_args) = type_args {
let last_segment = path.segment().unwrap();
path = path.with_segment(last_segment.with_type_args(type_args))
}
Some(path.syntax().clone())
}
PathResolution::Local(_)
| PathResolution::TypeParam(_)
| PathResolution::SelfType(_) => None,
PathResolution::Macro(_) => None,
PathResolution::AssocItem(_) => None,
}
}
}
impl<'a> AstTransform<'a> for QualifyPaths<'a> {
fn get_substitution(&self, node: &syntax::SyntaxNode) -> Option<syntax::SyntaxNode> {
self.get_substitution_inner(node).or_else(|| self.previous.get_substitution(node))
}
fn chain_before(self, other: Box<dyn AstTransform<'a> + 'a>) -> Box<dyn AstTransform<'a> + 'a> {
Box::new(QualifyPaths { previous: other, ..self })
}
}
pub(crate) fn path_to_ast(path: hir::ModPath) -> ast::Path {
let parse = ast::SourceFile::parse(&path.to_string());
parse
.tree()
.syntax()
.descendants()
.find_map(ast::Path::cast)
.unwrap_or_else(|| panic!("failed to parse path {:?}, `{}`", path, path))
}

View file

@ -0,0 +1,208 @@
use itertools::Itertools;
use syntax::{
ast::{self, AstNode},
Direction, SmolStr,
SyntaxKind::{IDENT, WHITESPACE},
TextRange, TextSize,
};
use crate::{
assist_context::{AssistContext, Assists},
AssistId, AssistKind,
};
// Assist: add_custom_impl
//
// Adds impl block for derived trait.
//
// ```
// #[derive(Deb<|>ug, Display)]
// struct S;
// ```
// ->
// ```
// #[derive(Display)]
// struct S;
//
// impl Debug for S {
// $0
// }
// ```
pub(crate) fn add_custom_impl(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
let attr = ctx.find_node_at_offset::<ast::Attr>()?;
let input = attr.token_tree()?;
let attr_name = attr
.syntax()
.descendants_with_tokens()
.filter(|t| t.kind() == IDENT)
.find_map(|i| i.into_token())
.filter(|t| *t.text() == "derive")?
.text()
.clone();
let trait_token =
ctx.token_at_offset().find(|t| t.kind() == IDENT && *t.text() != attr_name)?;
let annotated = attr.syntax().siblings(Direction::Next).find_map(ast::Name::cast)?;
let annotated_name = annotated.syntax().text().to_string();
let start_offset = annotated.syntax().parent()?.text_range().end();
let label =
format!("Add custom impl `{}` for `{}`", trait_token.text().as_str(), annotated_name);
let target = attr.syntax().text_range();
acc.add(AssistId("add_custom_impl", AssistKind::Refactor), label, target, |builder| {
let new_attr_input = input
.syntax()
.descendants_with_tokens()
.filter(|t| t.kind() == IDENT)
.filter_map(|t| t.into_token().map(|t| t.text().clone()))
.filter(|t| t != trait_token.text())
.collect::<Vec<SmolStr>>();
let has_more_derives = !new_attr_input.is_empty();
if has_more_derives {
let new_attr_input = format!("({})", new_attr_input.iter().format(", "));
builder.replace(input.syntax().text_range(), new_attr_input);
} else {
let attr_range = attr.syntax().text_range();
builder.delete(attr_range);
let line_break_range = attr
.syntax()
.next_sibling_or_token()
.filter(|t| t.kind() == WHITESPACE)
.map(|t| t.text_range())
.unwrap_or_else(|| TextRange::new(TextSize::from(0), TextSize::from(0)));
builder.delete(line_break_range);
}
match ctx.config.snippet_cap {
Some(cap) => {
builder.insert_snippet(
cap,
start_offset,
format!("\n\nimpl {} for {} {{\n $0\n}}", trait_token, annotated_name),
);
}
None => {
builder.insert(
start_offset,
format!("\n\nimpl {} for {} {{\n\n}}", trait_token, annotated_name),
);
}
}
})
}
#[cfg(test)]
mod tests {
use crate::tests::{check_assist, check_assist_not_applicable};
use super::*;
#[test]
fn add_custom_impl_for_unique_input() {
check_assist(
add_custom_impl,
"
#[derive(Debu<|>g)]
struct Foo {
bar: String,
}
",
"
struct Foo {
bar: String,
}
impl Debug for Foo {
$0
}
",
)
}
#[test]
fn add_custom_impl_for_with_visibility_modifier() {
check_assist(
add_custom_impl,
"
#[derive(Debug<|>)]
pub struct Foo {
bar: String,
}
",
"
pub struct Foo {
bar: String,
}
impl Debug for Foo {
$0
}
",
)
}
#[test]
fn add_custom_impl_when_multiple_inputs() {
check_assist(
add_custom_impl,
"
#[derive(Display, Debug<|>, Serialize)]
struct Foo {}
",
"
#[derive(Display, Serialize)]
struct Foo {}
impl Debug for Foo {
$0
}
",
)
}
#[test]
fn test_ignore_derive_macro_without_input() {
check_assist_not_applicable(
add_custom_impl,
"
#[derive(<|>)]
struct Foo {}
",
)
}
#[test]
fn test_ignore_if_cursor_on_param() {
check_assist_not_applicable(
add_custom_impl,
"
#[derive<|>(Debug)]
struct Foo {}
",
);
check_assist_not_applicable(
add_custom_impl,
"
#[derive(Debug)<|>]
struct Foo {}
",
)
}
#[test]
fn test_ignore_if_not_derive() {
check_assist_not_applicable(
add_custom_impl,
"
#[allow(non_camel_<|>case_types)]
struct Foo {}
",
)
}
}

View file

@ -0,0 +1,211 @@
use hir::HirDisplay;
use syntax::{
ast::{self, AstNode, LetStmt, NameOwner},
TextRange,
};
use crate::{AssistContext, AssistId, AssistKind, Assists};
// Assist: add_explicit_type
//
// Specify type for a let binding.
//
// ```
// fn main() {
// let x<|> = 92;
// }
// ```
// ->
// ```
// fn main() {
// let x: i32 = 92;
// }
// ```
pub(crate) fn add_explicit_type(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
let let_stmt = ctx.find_node_at_offset::<LetStmt>()?;
let module = ctx.sema.scope(let_stmt.syntax()).module()?;
let expr = let_stmt.initializer()?;
// Must be a binding
let pat = match let_stmt.pat()? {
ast::Pat::IdentPat(bind_pat) => bind_pat,
_ => return None,
};
let pat_range = pat.syntax().text_range();
// The binding must have a name
let name = pat.name()?;
let name_range = name.syntax().text_range();
let stmt_range = let_stmt.syntax().text_range();
let eq_range = let_stmt.eq_token()?.text_range();
// Assist should only be applicable if cursor is between 'let' and '='
let let_range = TextRange::new(stmt_range.start(), eq_range.start());
let cursor_in_range = let_range.contains_range(ctx.frange.range);
if !cursor_in_range {
return None;
}
// Assist not applicable if the type has already been specified
// and it has no placeholders
let ascribed_ty = let_stmt.ty();
if let Some(ty) = &ascribed_ty {
if ty.syntax().descendants().find_map(ast::InferType::cast).is_none() {
return None;
}
}
// Infer type
let ty = ctx.sema.type_of_expr(&expr)?;
if ty.contains_unknown() || ty.is_closure() {
return None;
}
let inferred_type = ty.display_source_code(ctx.db(), module.into()).ok()?;
acc.add(
AssistId("add_explicit_type", AssistKind::RefactorRewrite),
format!("Insert explicit type `{}`", inferred_type),
pat_range,
|builder| match ascribed_ty {
Some(ascribed_ty) => {
builder.replace(ascribed_ty.syntax().text_range(), inferred_type);
}
None => {
builder.insert(name_range.end(), format!(": {}", inferred_type));
}
},
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tests::{check_assist, check_assist_not_applicable, check_assist_target};
#[test]
fn add_explicit_type_target() {
check_assist_target(add_explicit_type, "fn f() { let a<|> = 1; }", "a");
}
#[test]
fn add_explicit_type_works_for_simple_expr() {
check_assist(add_explicit_type, "fn f() { let a<|> = 1; }", "fn f() { let a: i32 = 1; }");
}
#[test]
fn add_explicit_type_works_for_underscore() {
check_assist(
add_explicit_type,
"fn f() { let a<|>: _ = 1; }",
"fn f() { let a: i32 = 1; }",
);
}
#[test]
fn add_explicit_type_works_for_nested_underscore() {
check_assist(
add_explicit_type,
r#"
enum Option<T> {
Some(T),
None
}
fn f() {
let a<|>: Option<_> = Option::Some(1);
}"#,
r#"
enum Option<T> {
Some(T),
None
}
fn f() {
let a: Option<i32> = Option::Some(1);
}"#,
);
}
#[test]
fn add_explicit_type_works_for_macro_call() {
check_assist(
add_explicit_type,
r"macro_rules! v { () => {0u64} } fn f() { let a<|> = v!(); }",
r"macro_rules! v { () => {0u64} } fn f() { let a: u64 = v!(); }",
);
}
#[test]
fn add_explicit_type_works_for_macro_call_recursive() {
check_assist(
add_explicit_type,
r#"macro_rules! u { () => {0u64} } macro_rules! v { () => {u!()} } fn f() { let a<|> = v!(); }"#,
r#"macro_rules! u { () => {0u64} } macro_rules! v { () => {u!()} } fn f() { let a: u64 = v!(); }"#,
);
}
#[test]
fn add_explicit_type_not_applicable_if_ty_not_inferred() {
check_assist_not_applicable(add_explicit_type, "fn f() { let a<|> = None; }");
}
#[test]
fn add_explicit_type_not_applicable_if_ty_already_specified() {
check_assist_not_applicable(add_explicit_type, "fn f() { let a<|>: i32 = 1; }");
}
#[test]
fn add_explicit_type_not_applicable_if_specified_ty_is_tuple() {
check_assist_not_applicable(add_explicit_type, "fn f() { let a<|>: (i32, i32) = (3, 4); }");
}
#[test]
fn add_explicit_type_not_applicable_if_cursor_after_equals() {
check_assist_not_applicable(
add_explicit_type,
"fn f() {let a =<|> match 1 {2 => 3, 3 => 5};}",
)
}
#[test]
fn add_explicit_type_not_applicable_if_cursor_before_let() {
check_assist_not_applicable(
add_explicit_type,
"fn f() <|>{let a = match 1 {2 => 3, 3 => 5};}",
)
}
#[test]
fn closure_parameters_are_not_added() {
check_assist_not_applicable(
add_explicit_type,
r#"
fn main() {
let multiply_by_two<|> = |i| i * 3;
let six = multiply_by_two(2);
}"#,
)
}
#[test]
fn default_generics_should_not_be_added() {
check_assist(
add_explicit_type,
r#"
struct Test<K, T = u8> {
k: K,
t: T,
}
fn main() {
let test<|> = Test { t: 23u8, k: 33 };
}"#,
r#"
struct Test<K, T = u8> {
k: K,
t: T,
}
fn main() {
let test: Test<i32> = Test { t: 23u8, k: 33 };
}"#,
);
}
}

View file

@ -0,0 +1,766 @@
use hir::HasSource;
use syntax::{
ast::{
self,
edit::{self, AstNodeEdit, IndentLevel},
make, AstNode, NameOwner,
},
SmolStr,
};
use crate::{
assist_context::{AssistContext, Assists},
ast_transform::{self, AstTransform, QualifyPaths, SubstituteTypeParams},
utils::{get_missing_assoc_items, render_snippet, resolve_target_trait, Cursor},
AssistId, AssistKind,
};
#[derive(PartialEq)]
enum AddMissingImplMembersMode {
DefaultMethodsOnly,
NoDefaultMethods,
}
// Assist: add_impl_missing_members
//
// Adds scaffold for required impl members.
//
// ```
// trait Trait<T> {
// Type X;
// fn foo(&self) -> T;
// fn bar(&self) {}
// }
//
// impl Trait<u32> for () {<|>
//
// }
// ```
// ->
// ```
// trait Trait<T> {
// Type X;
// fn foo(&self) -> T;
// fn bar(&self) {}
// }
//
// impl Trait<u32> for () {
// fn foo(&self) -> u32 {
// ${0:todo!()}
// }
// }
// ```
pub(crate) fn add_missing_impl_members(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
add_missing_impl_members_inner(
acc,
ctx,
AddMissingImplMembersMode::NoDefaultMethods,
"add_impl_missing_members",
"Implement missing members",
)
}
// Assist: add_impl_default_members
//
// Adds scaffold for overriding default impl members.
//
// ```
// trait Trait {
// Type X;
// fn foo(&self);
// fn bar(&self) {}
// }
//
// impl Trait for () {
// Type X = ();
// fn foo(&self) {}<|>
//
// }
// ```
// ->
// ```
// trait Trait {
// Type X;
// fn foo(&self);
// fn bar(&self) {}
// }
//
// impl Trait for () {
// Type X = ();
// fn foo(&self) {}
//
// $0fn bar(&self) {}
// }
// ```
pub(crate) fn add_missing_default_members(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
add_missing_impl_members_inner(
acc,
ctx,
AddMissingImplMembersMode::DefaultMethodsOnly,
"add_impl_default_members",
"Implement default members",
)
}
fn add_missing_impl_members_inner(
acc: &mut Assists,
ctx: &AssistContext,
mode: AddMissingImplMembersMode,
assist_id: &'static str,
label: &'static str,
) -> Option<()> {
let _p = profile::span("add_missing_impl_members_inner");
let impl_def = ctx.find_node_at_offset::<ast::Impl>()?;
let impl_item_list = impl_def.assoc_item_list()?;
let trait_ = resolve_target_trait(&ctx.sema, &impl_def)?;
let def_name = |item: &ast::AssocItem| -> Option<SmolStr> {
match item {
ast::AssocItem::Fn(def) => def.name(),
ast::AssocItem::TypeAlias(def) => def.name(),
ast::AssocItem::Const(def) => def.name(),
ast::AssocItem::MacroCall(_) => None,
}
.map(|it| it.text().clone())
};
let missing_items = get_missing_assoc_items(&ctx.sema, &impl_def)
.iter()
.map(|i| match i {
hir::AssocItem::Function(i) => ast::AssocItem::Fn(i.source(ctx.db()).value),
hir::AssocItem::TypeAlias(i) => ast::AssocItem::TypeAlias(i.source(ctx.db()).value),
hir::AssocItem::Const(i) => ast::AssocItem::Const(i.source(ctx.db()).value),
})
.filter(|t| def_name(&t).is_some())
.filter(|t| match t {
ast::AssocItem::Fn(def) => match mode {
AddMissingImplMembersMode::DefaultMethodsOnly => def.body().is_some(),
AddMissingImplMembersMode::NoDefaultMethods => def.body().is_none(),
},
_ => mode == AddMissingImplMembersMode::NoDefaultMethods,
})
.collect::<Vec<_>>();
if missing_items.is_empty() {
return None;
}
let target = impl_def.syntax().text_range();
acc.add(AssistId(assist_id, AssistKind::QuickFix), label, target, |builder| {
let n_existing_items = impl_item_list.assoc_items().count();
let source_scope = ctx.sema.scope_for_def(trait_);
let target_scope = ctx.sema.scope(impl_item_list.syntax());
let ast_transform = QualifyPaths::new(&target_scope, &source_scope)
.or(SubstituteTypeParams::for_trait_impl(&source_scope, trait_, impl_def));
let items = missing_items
.into_iter()
.map(|it| ast_transform::apply(&*ast_transform, it))
.map(|it| match it {
ast::AssocItem::Fn(def) => ast::AssocItem::Fn(add_body(def)),
ast::AssocItem::TypeAlias(def) => ast::AssocItem::TypeAlias(def.remove_bounds()),
_ => it,
})
.map(|it| edit::remove_attrs_and_docs(&it));
let new_impl_item_list = impl_item_list.append_items(items);
let first_new_item = new_impl_item_list.assoc_items().nth(n_existing_items).unwrap();
let original_range = impl_item_list.syntax().text_range();
match ctx.config.snippet_cap {
None => builder.replace(original_range, new_impl_item_list.to_string()),
Some(cap) => {
let mut cursor = Cursor::Before(first_new_item.syntax());
let placeholder;
if let ast::AssocItem::Fn(func) = &first_new_item {
if let Some(m) = func.syntax().descendants().find_map(ast::MacroCall::cast) {
if m.syntax().text() == "todo!()" {
placeholder = m;
cursor = Cursor::Replace(placeholder.syntax());
}
}
}
builder.replace_snippet(
cap,
original_range,
render_snippet(cap, new_impl_item_list.syntax(), cursor),
)
}
};
})
}
fn add_body(fn_def: ast::Fn) -> ast::Fn {
if fn_def.body().is_some() {
return fn_def;
}
let body = make::block_expr(None, Some(make::expr_todo())).indent(IndentLevel(1));
fn_def.with_body(body)
}
#[cfg(test)]
mod tests {
use crate::tests::{check_assist, check_assist_not_applicable};
use super::*;
#[test]
fn test_add_missing_impl_members() {
check_assist(
add_missing_impl_members,
r#"
trait Foo {
type Output;
const CONST: usize = 42;
fn foo(&self);
fn bar(&self);
fn baz(&self);
}
struct S;
impl Foo for S {
fn bar(&self) {}
<|>
}"#,
r#"
trait Foo {
type Output;
const CONST: usize = 42;
fn foo(&self);
fn bar(&self);
fn baz(&self);
}
struct S;
impl Foo for S {
fn bar(&self) {}
$0type Output;
const CONST: usize = 42;
fn foo(&self) {
todo!()
}
fn baz(&self) {
todo!()
}
}"#,
);
}
#[test]
fn test_copied_overriden_members() {
check_assist(
add_missing_impl_members,
r#"
trait Foo {
fn foo(&self);
fn bar(&self) -> bool { true }
fn baz(&self) -> u32 { 42 }
}
struct S;
impl Foo for S {
fn bar(&self) {}
<|>
}"#,
r#"
trait Foo {
fn foo(&self);
fn bar(&self) -> bool { true }
fn baz(&self) -> u32 { 42 }
}
struct S;
impl Foo for S {
fn bar(&self) {}
fn foo(&self) {
${0:todo!()}
}
}"#,
);
}
#[test]
fn test_empty_impl_def() {
check_assist(
add_missing_impl_members,
r#"
trait Foo { fn foo(&self); }
struct S;
impl Foo for S { <|> }"#,
r#"
trait Foo { fn foo(&self); }
struct S;
impl Foo for S {
fn foo(&self) {
${0:todo!()}
}
}"#,
);
}
#[test]
fn fill_in_type_params_1() {
check_assist(
add_missing_impl_members,
r#"
trait Foo<T> { fn foo(&self, t: T) -> &T; }
struct S;
impl Foo<u32> for S { <|> }"#,
r#"
trait Foo<T> { fn foo(&self, t: T) -> &T; }
struct S;
impl Foo<u32> for S {
fn foo(&self, t: u32) -> &u32 {
${0:todo!()}
}
}"#,
);
}
#[test]
fn fill_in_type_params_2() {
check_assist(
add_missing_impl_members,
r#"
trait Foo<T> { fn foo(&self, t: T) -> &T; }
struct S;
impl<U> Foo<U> for S { <|> }"#,
r#"
trait Foo<T> { fn foo(&self, t: T) -> &T; }
struct S;
impl<U> Foo<U> for S {
fn foo(&self, t: U) -> &U {
${0:todo!()}
}
}"#,
);
}
#[test]
fn test_cursor_after_empty_impl_def() {
check_assist(
add_missing_impl_members,
r#"
trait Foo { fn foo(&self); }
struct S;
impl Foo for S {}<|>"#,
r#"
trait Foo { fn foo(&self); }
struct S;
impl Foo for S {
fn foo(&self) {
${0:todo!()}
}
}"#,
)
}
#[test]
fn test_qualify_path_1() {
check_assist(
add_missing_impl_members,
r#"
mod foo {
pub struct Bar;
trait Foo { fn foo(&self, bar: Bar); }
}
struct S;
impl foo::Foo for S { <|> }"#,
r#"
mod foo {
pub struct Bar;
trait Foo { fn foo(&self, bar: Bar); }
}
struct S;
impl foo::Foo for S {
fn foo(&self, bar: foo::Bar) {
${0:todo!()}
}
}"#,
);
}
#[test]
fn test_qualify_path_generic() {
check_assist(
add_missing_impl_members,
r#"
mod foo {
pub struct Bar<T>;
trait Foo { fn foo(&self, bar: Bar<u32>); }
}
struct S;
impl foo::Foo for S { <|> }"#,
r#"
mod foo {
pub struct Bar<T>;
trait Foo { fn foo(&self, bar: Bar<u32>); }
}
struct S;
impl foo::Foo for S {
fn foo(&self, bar: foo::Bar<u32>) {
${0:todo!()}
}
}"#,
);
}
#[test]
fn test_qualify_path_and_substitute_param() {
check_assist(
add_missing_impl_members,
r#"
mod foo {
pub struct Bar<T>;
trait Foo<T> { fn foo(&self, bar: Bar<T>); }
}
struct S;
impl foo::Foo<u32> for S { <|> }"#,
r#"
mod foo {
pub struct Bar<T>;
trait Foo<T> { fn foo(&self, bar: Bar<T>); }
}
struct S;
impl foo::Foo<u32> for S {
fn foo(&self, bar: foo::Bar<u32>) {
${0:todo!()}
}
}"#,
);
}
#[test]
fn test_substitute_param_no_qualify() {
// when substituting params, the substituted param should not be qualified!
check_assist(
add_missing_impl_members,
r#"
mod foo {
trait Foo<T> { fn foo(&self, bar: T); }
pub struct Param;
}
struct Param;
struct S;
impl foo::Foo<Param> for S { <|> }"#,
r#"
mod foo {
trait Foo<T> { fn foo(&self, bar: T); }
pub struct Param;
}
struct Param;
struct S;
impl foo::Foo<Param> for S {
fn foo(&self, bar: Param) {
${0:todo!()}
}
}"#,
);
}
#[test]
fn test_qualify_path_associated_item() {
check_assist(
add_missing_impl_members,
r#"
mod foo {
pub struct Bar<T>;
impl Bar<T> { type Assoc = u32; }
trait Foo { fn foo(&self, bar: Bar<u32>::Assoc); }
}
struct S;
impl foo::Foo for S { <|> }"#,
r#"
mod foo {
pub struct Bar<T>;
impl Bar<T> { type Assoc = u32; }
trait Foo { fn foo(&self, bar: Bar<u32>::Assoc); }
}
struct S;
impl foo::Foo for S {
fn foo(&self, bar: foo::Bar<u32>::Assoc) {
${0:todo!()}
}
}"#,
);
}
#[test]
fn test_qualify_path_nested() {
check_assist(
add_missing_impl_members,
r#"
mod foo {
pub struct Bar<T>;
pub struct Baz;
trait Foo { fn foo(&self, bar: Bar<Baz>); }
}
struct S;
impl foo::Foo for S { <|> }"#,
r#"
mod foo {
pub struct Bar<T>;
pub struct Baz;
trait Foo { fn foo(&self, bar: Bar<Baz>); }
}
struct S;
impl foo::Foo for S {
fn foo(&self, bar: foo::Bar<foo::Baz>) {
${0:todo!()}
}
}"#,
);
}
#[test]
fn test_qualify_path_fn_trait_notation() {
check_assist(
add_missing_impl_members,
r#"
mod foo {
pub trait Fn<Args> { type Output; }
trait Foo { fn foo(&self, bar: dyn Fn(u32) -> i32); }
}
struct S;
impl foo::Foo for S { <|> }"#,
r#"
mod foo {
pub trait Fn<Args> { type Output; }
trait Foo { fn foo(&self, bar: dyn Fn(u32) -> i32); }
}
struct S;
impl foo::Foo for S {
fn foo(&self, bar: dyn Fn(u32) -> i32) {
${0:todo!()}
}
}"#,
);
}
#[test]
fn test_empty_trait() {
check_assist_not_applicable(
add_missing_impl_members,
r#"
trait Foo;
struct S;
impl Foo for S { <|> }"#,
)
}
#[test]
fn test_ignore_unnamed_trait_members_and_default_methods() {
check_assist_not_applicable(
add_missing_impl_members,
r#"
trait Foo {
fn (arg: u32);
fn valid(some: u32) -> bool { false }
}
struct S;
impl Foo for S { <|> }"#,
)
}
#[test]
fn test_with_docstring_and_attrs() {
check_assist(
add_missing_impl_members,
r#"
#[doc(alias = "test alias")]
trait Foo {
/// doc string
type Output;
#[must_use]
fn foo(&self);
}
struct S;
impl Foo for S {}<|>"#,
r#"
#[doc(alias = "test alias")]
trait Foo {
/// doc string
type Output;
#[must_use]
fn foo(&self);
}
struct S;
impl Foo for S {
$0type Output;
fn foo(&self) {
todo!()
}
}"#,
)
}
#[test]
fn test_default_methods() {
check_assist(
add_missing_default_members,
r#"
trait Foo {
type Output;
const CONST: usize = 42;
fn valid(some: u32) -> bool { false }
fn foo(some: u32) -> bool;
}
struct S;
impl Foo for S { <|> }"#,
r#"
trait Foo {
type Output;
const CONST: usize = 42;
fn valid(some: u32) -> bool { false }
fn foo(some: u32) -> bool;
}
struct S;
impl Foo for S {
$0fn valid(some: u32) -> bool { false }
}"#,
)
}
#[test]
fn test_generic_single_default_parameter() {
check_assist(
add_missing_impl_members,
r#"
trait Foo<T = Self> {
fn bar(&self, other: &T);
}
struct S;
impl Foo for S { <|> }"#,
r#"
trait Foo<T = Self> {
fn bar(&self, other: &T);
}
struct S;
impl Foo for S {
fn bar(&self, other: &Self) {
${0:todo!()}
}
}"#,
)
}
#[test]
fn test_generic_default_parameter_is_second() {
check_assist(
add_missing_impl_members,
r#"
trait Foo<T1, T2 = Self> {
fn bar(&self, this: &T1, that: &T2);
}
struct S<T>;
impl Foo<T> for S<T> { <|> }"#,
r#"
trait Foo<T1, T2 = Self> {
fn bar(&self, this: &T1, that: &T2);
}
struct S<T>;
impl Foo<T> for S<T> {
fn bar(&self, this: &T, that: &Self) {
${0:todo!()}
}
}"#,
)
}
#[test]
fn test_assoc_type_bounds_are_removed() {
check_assist(
add_missing_impl_members,
r#"
trait Tr {
type Ty: Copy + 'static;
}
impl Tr for ()<|> {
}"#,
r#"
trait Tr {
type Ty: Copy + 'static;
}
impl Tr for () {
$0type Ty;
}"#,
)
}
#[test]
fn test_whitespace_fixup_preserves_bad_tokens() {
check_assist(
add_missing_impl_members,
r#"
trait Tr {
fn foo();
}
impl Tr for ()<|> {
+++
}"#,
r#"
trait Tr {
fn foo();
}
impl Tr for () {
fn foo() {
${0:todo!()}
}
+++
}"#,
)
}
#[test]
fn test_whitespace_fixup_preserves_comments() {
check_assist(
add_missing_impl_members,
r#"
trait Tr {
fn foo();
}
impl Tr for ()<|> {
// very important
}"#,
r#"
trait Tr {
fn foo();
}
impl Tr for () {
fn foo() {
${0:todo!()}
}
// very important
}"#,
)
}
}

View file

@ -0,0 +1,164 @@
use ide_db::defs::{classify_name_ref, Definition, NameRefClass};
use syntax::{ast, AstNode, SyntaxKind, T};
use test_utils::mark;
use crate::{
assist_context::{AssistContext, Assists},
AssistId, AssistKind,
};
// Assist: add_turbo_fish
//
// Adds `::<_>` to a call of a generic method or function.
//
// ```
// fn make<T>() -> T { todo!() }
// fn main() {
// let x = make<|>();
// }
// ```
// ->
// ```
// fn make<T>() -> T { todo!() }
// fn main() {
// let x = make::<${0:_}>();
// }
// ```
pub(crate) fn add_turbo_fish(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
let ident = ctx.find_token_at_offset(SyntaxKind::IDENT).or_else(|| {
let arg_list = ctx.find_node_at_offset::<ast::ArgList>()?;
if arg_list.args().count() > 0 {
return None;
}
mark::hit!(add_turbo_fish_after_call);
arg_list.l_paren_token()?.prev_token().filter(|it| it.kind() == SyntaxKind::IDENT)
})?;
let next_token = ident.next_token()?;
if next_token.kind() == T![::] {
mark::hit!(add_turbo_fish_one_fish_is_enough);
return None;
}
let name_ref = ast::NameRef::cast(ident.parent())?;
let def = match classify_name_ref(&ctx.sema, &name_ref)? {
NameRefClass::Definition(def) => def,
NameRefClass::ExternCrate(_) | NameRefClass::FieldShorthand { .. } => return None,
};
let fun = match def {
Definition::ModuleDef(hir::ModuleDef::Function(it)) => it,
_ => return None,
};
let generics = hir::GenericDef::Function(fun).params(ctx.sema.db);
if generics.is_empty() {
mark::hit!(add_turbo_fish_non_generic);
return None;
}
acc.add(
AssistId("add_turbo_fish", AssistKind::RefactorRewrite),
"Add `::<>`",
ident.text_range(),
|builder| match ctx.config.snippet_cap {
Some(cap) => builder.insert_snippet(cap, ident.text_range().end(), "::<${0:_}>"),
None => builder.insert(ident.text_range().end(), "::<_>"),
},
)
}
#[cfg(test)]
mod tests {
use crate::tests::{check_assist, check_assist_not_applicable};
use super::*;
use test_utils::mark;
#[test]
fn add_turbo_fish_function() {
check_assist(
add_turbo_fish,
r#"
fn make<T>() -> T {}
fn main() {
make<|>();
}
"#,
r#"
fn make<T>() -> T {}
fn main() {
make::<${0:_}>();
}
"#,
);
}
#[test]
fn add_turbo_fish_after_call() {
mark::check!(add_turbo_fish_after_call);
check_assist(
add_turbo_fish,
r#"
fn make<T>() -> T {}
fn main() {
make()<|>;
}
"#,
r#"
fn make<T>() -> T {}
fn main() {
make::<${0:_}>();
}
"#,
);
}
#[test]
fn add_turbo_fish_method() {
check_assist(
add_turbo_fish,
r#"
struct S;
impl S {
fn make<T>(&self) -> T {}
}
fn main() {
S.make<|>();
}
"#,
r#"
struct S;
impl S {
fn make<T>(&self) -> T {}
}
fn main() {
S.make::<${0:_}>();
}
"#,
);
}
#[test]
fn add_turbo_fish_one_fish_is_enough() {
mark::check!(add_turbo_fish_one_fish_is_enough);
check_assist_not_applicable(
add_turbo_fish,
r#"
fn make<T>() -> T {}
fn main() {
make<|>::<()>();
}
"#,
);
}
#[test]
fn add_turbo_fish_non_generic() {
mark::check!(add_turbo_fish_non_generic);
check_assist_not_applicable(
add_turbo_fish,
r#"
fn make() -> () {}
fn main() {
make<|>();
}
"#,
);
}
}

View file

@ -0,0 +1,93 @@
use syntax::ast::{self, AstNode};
use crate::{utils::invert_boolean_expression, AssistContext, AssistId, AssistKind, Assists};
// Assist: apply_demorgan
//
// Apply https://en.wikipedia.org/wiki/De_Morgan%27s_laws[De Morgan's law].
// This transforms expressions of the form `!l || !r` into `!(l && r)`.
// This also works with `&&`. This assist can only be applied with the cursor
// on either `||` or `&&`, with both operands being a negation of some kind.
// This means something of the form `!x` or `x != y`.
//
// ```
// fn main() {
// if x != 4 ||<|> !y {}
// }
// ```
// ->
// ```
// fn main() {
// if !(x == 4 && y) {}
// }
// ```
pub(crate) fn apply_demorgan(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
let expr = ctx.find_node_at_offset::<ast::BinExpr>()?;
let op = expr.op_kind()?;
let op_range = expr.op_token()?.text_range();
let opposite_op = opposite_logic_op(op)?;
let cursor_in_range = op_range.contains_range(ctx.frange.range);
if !cursor_in_range {
return None;
}
let lhs = expr.lhs()?;
let lhs_range = lhs.syntax().text_range();
let not_lhs = invert_boolean_expression(lhs);
let rhs = expr.rhs()?;
let rhs_range = rhs.syntax().text_range();
let not_rhs = invert_boolean_expression(rhs);
acc.add(
AssistId("apply_demorgan", AssistKind::RefactorRewrite),
"Apply De Morgan's law",
op_range,
|edit| {
edit.replace(op_range, opposite_op);
edit.replace(lhs_range, format!("!({}", not_lhs.syntax().text()));
edit.replace(rhs_range, format!("{})", not_rhs.syntax().text()));
},
)
}
// Return the opposite text for a given logical operator, if it makes sense
fn opposite_logic_op(kind: ast::BinOp) -> Option<&'static str> {
match kind {
ast::BinOp::BooleanOr => Some("&&"),
ast::BinOp::BooleanAnd => Some("||"),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tests::{check_assist, check_assist_not_applicable};
#[test]
fn demorgan_turns_and_into_or() {
check_assist(apply_demorgan, "fn f() { !x &&<|> !x }", "fn f() { !(x || x) }")
}
#[test]
fn demorgan_turns_or_into_and() {
check_assist(apply_demorgan, "fn f() { !x ||<|> !x }", "fn f() { !(x && x) }")
}
#[test]
fn demorgan_removes_inequality() {
check_assist(apply_demorgan, "fn f() { x != x ||<|> !x }", "fn f() { !(x == x && x) }")
}
#[test]
fn demorgan_general_case() {
check_assist(apply_demorgan, "fn f() { x ||<|> x }", "fn f() { !(!x && !x) }")
}
#[test]
fn demorgan_doesnt_apply_with_cursor_not_on_op() {
check_assist_not_applicable(apply_demorgan, "fn f() { <|> !x || !x }")
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,998 @@
use std::iter;
use syntax::{
ast::{self, make, BlockExpr, Expr, LoopBodyOwner},
AstNode, SyntaxNode,
};
use test_utils::mark;
use crate::{AssistContext, AssistId, AssistKind, Assists};
// Assist: change_return_type_to_result
//
// Change the function's return type to Result.
//
// ```
// fn foo() -> i32<|> { 42i32 }
// ```
// ->
// ```
// fn foo() -> Result<i32, ${0:_}> { Ok(42i32) }
// ```
pub(crate) fn change_return_type_to_result(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
let ret_type = ctx.find_node_at_offset::<ast::RetType>()?;
// FIXME: extend to lambdas as well
let fn_def = ret_type.syntax().parent().and_then(ast::Fn::cast)?;
let type_ref = &ret_type.ty()?;
let ret_type_str = type_ref.syntax().text().to_string();
let first_part_ret_type = ret_type_str.splitn(2, '<').next();
if let Some(ret_type_first_part) = first_part_ret_type {
if ret_type_first_part.ends_with("Result") {
mark::hit!(change_return_type_to_result_simple_return_type_already_result);
return None;
}
}
let block_expr = &fn_def.body()?;
acc.add(
AssistId("change_return_type_to_result", AssistKind::RefactorRewrite),
"Wrap return type in Result",
type_ref.syntax().text_range(),
|builder| {
let mut tail_return_expr_collector = TailReturnCollector::new();
tail_return_expr_collector.collect_jump_exprs(block_expr, false);
tail_return_expr_collector.collect_tail_exprs(block_expr);
for ret_expr_arg in tail_return_expr_collector.exprs_to_wrap {
let ok_wrapped = make::expr_call(
make::expr_path(make::path_unqualified(make::path_segment(make::name_ref(
"Ok",
)))),
make::arg_list(iter::once(ret_expr_arg.clone())),
);
builder.replace_ast(ret_expr_arg, ok_wrapped);
}
match ctx.config.snippet_cap {
Some(cap) => {
let snippet = format!("Result<{}, ${{0:_}}>", type_ref);
builder.replace_snippet(cap, type_ref.syntax().text_range(), snippet)
}
None => builder
.replace(type_ref.syntax().text_range(), format!("Result<{}, _>", type_ref)),
}
},
)
}
struct TailReturnCollector {
exprs_to_wrap: Vec<ast::Expr>,
}
impl TailReturnCollector {
fn new() -> Self {
Self { exprs_to_wrap: vec![] }
}
/// Collect all`return` expression
fn collect_jump_exprs(&mut self, block_expr: &BlockExpr, collect_break: bool) {
let statements = block_expr.statements();
for stmt in statements {
let expr = match &stmt {
ast::Stmt::ExprStmt(stmt) => stmt.expr(),
ast::Stmt::LetStmt(stmt) => stmt.initializer(),
ast::Stmt::Item(_) => continue,
};
if let Some(expr) = &expr {
self.handle_exprs(expr, collect_break);
}
}
// Browse tail expressions for each block
if let Some(expr) = block_expr.expr() {
if let Some(last_exprs) = get_tail_expr_from_block(&expr) {
for last_expr in last_exprs {
let last_expr = match last_expr {
NodeType::Node(expr) => expr,
NodeType::Leaf(expr) => expr.syntax().clone(),
};
if let Some(last_expr) = Expr::cast(last_expr.clone()) {
self.handle_exprs(&last_expr, collect_break);
} else if let Some(expr_stmt) = ast::Stmt::cast(last_expr) {
let expr_stmt = match &expr_stmt {
ast::Stmt::ExprStmt(stmt) => stmt.expr(),
ast::Stmt::LetStmt(stmt) => stmt.initializer(),
ast::Stmt::Item(_) => None,
};
if let Some(expr) = &expr_stmt {
self.handle_exprs(expr, collect_break);
}
}
}
}
}
}
fn handle_exprs(&mut self, expr: &Expr, collect_break: bool) {
match expr {
Expr::BlockExpr(block_expr) => {
self.collect_jump_exprs(&block_expr, collect_break);
}
Expr::ReturnExpr(ret_expr) => {
if let Some(ret_expr_arg) = &ret_expr.expr() {
self.exprs_to_wrap.push(ret_expr_arg.clone());
}
}
Expr::BreakExpr(break_expr) if collect_break => {
if let Some(break_expr_arg) = &break_expr.expr() {
self.exprs_to_wrap.push(break_expr_arg.clone());
}
}
Expr::IfExpr(if_expr) => {
for block in if_expr.blocks() {
self.collect_jump_exprs(&block, collect_break);
}
}
Expr::LoopExpr(loop_expr) => {
if let Some(block_expr) = loop_expr.loop_body() {
self.collect_jump_exprs(&block_expr, collect_break);
}
}
Expr::ForExpr(for_expr) => {
if let Some(block_expr) = for_expr.loop_body() {
self.collect_jump_exprs(&block_expr, collect_break);
}
}
Expr::WhileExpr(while_expr) => {
if let Some(block_expr) = while_expr.loop_body() {
self.collect_jump_exprs(&block_expr, collect_break);
}
}
Expr::MatchExpr(match_expr) => {
if let Some(arm_list) = match_expr.match_arm_list() {
arm_list.arms().filter_map(|match_arm| match_arm.expr()).for_each(|expr| {
self.handle_exprs(&expr, collect_break);
});
}
}
_ => {}
}
}
fn collect_tail_exprs(&mut self, block: &BlockExpr) {
if let Some(expr) = block.expr() {
self.handle_exprs(&expr, true);
self.fetch_tail_exprs(&expr);
}
}
fn fetch_tail_exprs(&mut self, expr: &Expr) {
if let Some(exprs) = get_tail_expr_from_block(expr) {
for node_type in &exprs {
match node_type {
NodeType::Leaf(expr) => {
self.exprs_to_wrap.push(expr.clone());
}
NodeType::Node(expr) => {
if let Some(last_expr) = Expr::cast(expr.clone()) {
self.fetch_tail_exprs(&last_expr);
}
}
}
}
}
}
}
#[derive(Debug)]
enum NodeType {
Leaf(ast::Expr),
Node(SyntaxNode),
}
/// Get a tail expression inside a block
fn get_tail_expr_from_block(expr: &Expr) -> Option<Vec<NodeType>> {
match expr {
Expr::IfExpr(if_expr) => {
let mut nodes = vec![];
for block in if_expr.blocks() {
if let Some(block_expr) = block.expr() {
if let Some(tail_exprs) = get_tail_expr_from_block(&block_expr) {
nodes.extend(tail_exprs);
}
} else if let Some(last_expr) = block.syntax().last_child() {
nodes.push(NodeType::Node(last_expr));
} else {
nodes.push(NodeType::Node(block.syntax().clone()));
}
}
Some(nodes)
}
Expr::LoopExpr(loop_expr) => {
loop_expr.syntax().last_child().map(|lc| vec![NodeType::Node(lc)])
}
Expr::ForExpr(for_expr) => {
for_expr.syntax().last_child().map(|lc| vec![NodeType::Node(lc)])
}
Expr::WhileExpr(while_expr) => {
while_expr.syntax().last_child().map(|lc| vec![NodeType::Node(lc)])
}
Expr::BlockExpr(block_expr) => {
block_expr.expr().map(|lc| vec![NodeType::Node(lc.syntax().clone())])
}
Expr::MatchExpr(match_expr) => {
let arm_list = match_expr.match_arm_list()?;
let arms: Vec<NodeType> = arm_list
.arms()
.filter_map(|match_arm| match_arm.expr())
.map(|expr| match expr {
Expr::ReturnExpr(ret_expr) => NodeType::Node(ret_expr.syntax().clone()),
Expr::BreakExpr(break_expr) => NodeType::Node(break_expr.syntax().clone()),
_ => match expr.syntax().last_child() {
Some(last_expr) => NodeType::Node(last_expr),
None => NodeType::Node(expr.syntax().clone()),
},
})
.collect();
Some(arms)
}
Expr::BreakExpr(expr) => expr.expr().map(|e| vec![NodeType::Leaf(e)]),
Expr::ReturnExpr(ret_expr) => Some(vec![NodeType::Node(ret_expr.syntax().clone())]),
Expr::CallExpr(_)
| Expr::Literal(_)
| Expr::TupleExpr(_)
| Expr::ArrayExpr(_)
| Expr::ParenExpr(_)
| Expr::PathExpr(_)
| Expr::RecordExpr(_)
| Expr::IndexExpr(_)
| Expr::MethodCallExpr(_)
| Expr::AwaitExpr(_)
| Expr::CastExpr(_)
| Expr::RefExpr(_)
| Expr::PrefixExpr(_)
| Expr::RangeExpr(_)
| Expr::BinExpr(_)
| Expr::MacroCall(_)
| Expr::BoxExpr(_) => Some(vec![NodeType::Leaf(expr.clone())]),
_ => None,
}
}
#[cfg(test)]
mod tests {
use crate::tests::{check_assist, check_assist_not_applicable};
use super::*;
#[test]
fn change_return_type_to_result_simple() {
check_assist(
change_return_type_to_result,
r#"fn foo() -> i3<|>2 {
let test = "test";
return 42i32;
}"#,
r#"fn foo() -> Result<i32, ${0:_}> {
let test = "test";
return Ok(42i32);
}"#,
);
}
#[test]
fn change_return_type_to_result_simple_return_type() {
check_assist(
change_return_type_to_result,
r#"fn foo() -> i32<|> {
let test = "test";
return 42i32;
}"#,
r#"fn foo() -> Result<i32, ${0:_}> {
let test = "test";
return Ok(42i32);
}"#,
);
}
#[test]
fn change_return_type_to_result_simple_return_type_bad_cursor() {
check_assist_not_applicable(
change_return_type_to_result,
r#"fn foo() -> i32 {
let test = "test";<|>
return 42i32;
}"#,
);
}
#[test]
fn change_return_type_to_result_simple_return_type_already_result_std() {
check_assist_not_applicable(
change_return_type_to_result,
r#"fn foo() -> std::result::Result<i32<|>, String> {
let test = "test";
return 42i32;
}"#,
);
}
#[test]
fn change_return_type_to_result_simple_return_type_already_result() {
mark::check!(change_return_type_to_result_simple_return_type_already_result);
check_assist_not_applicable(
change_return_type_to_result,
r#"fn foo() -> Result<i32<|>, String> {
let test = "test";
return 42i32;
}"#,
);
}
#[test]
fn change_return_type_to_result_simple_with_cursor() {
check_assist(
change_return_type_to_result,
r#"fn foo() -> <|>i32 {
let test = "test";
return 42i32;
}"#,
r#"fn foo() -> Result<i32, ${0:_}> {
let test = "test";
return Ok(42i32);
}"#,
);
}
#[test]
fn change_return_type_to_result_simple_with_tail() {
check_assist(
change_return_type_to_result,
r#"fn foo() -><|> i32 {
let test = "test";
42i32
}"#,
r#"fn foo() -> Result<i32, ${0:_}> {
let test = "test";
Ok(42i32)
}"#,
);
}
#[test]
fn change_return_type_to_result_simple_with_tail_only() {
check_assist(
change_return_type_to_result,
r#"fn foo() -> i32<|> {
42i32
}"#,
r#"fn foo() -> Result<i32, ${0:_}> {
Ok(42i32)
}"#,
);
}
#[test]
fn change_return_type_to_result_simple_with_tail_block_like() {
check_assist(
change_return_type_to_result,
r#"fn foo() -> i32<|> {
if true {
42i32
} else {
24i32
}
}"#,
r#"fn foo() -> Result<i32, ${0:_}> {
if true {
Ok(42i32)
} else {
Ok(24i32)
}
}"#,
);
}
#[test]
fn change_return_type_to_result_simple_with_nested_if() {
check_assist(
change_return_type_to_result,
r#"fn foo() -> i32<|> {
if true {
if false {
1
} else {
2
}
} else {
24i32
}
}"#,
r#"fn foo() -> Result<i32, ${0:_}> {
if true {
if false {
Ok(1)
} else {
Ok(2)
}
} else {
Ok(24i32)
}
}"#,
);
}
#[test]
fn change_return_type_to_result_simple_with_await() {
check_assist(
change_return_type_to_result,
r#"async fn foo() -> i<|>32 {
if true {
if false {
1.await
} else {
2.await
}
} else {
24i32.await
}
}"#,
r#"async fn foo() -> Result<i32, ${0:_}> {
if true {
if false {
Ok(1.await)
} else {
Ok(2.await)
}
} else {
Ok(24i32.await)
}
}"#,
);
}
#[test]
fn change_return_type_to_result_simple_with_array() {
check_assist(
change_return_type_to_result,
r#"fn foo() -> [i32;<|> 3] {
[1, 2, 3]
}"#,
r#"fn foo() -> Result<[i32; 3], ${0:_}> {
Ok([1, 2, 3])
}"#,
);
}
#[test]
fn change_return_type_to_result_simple_with_cast() {
check_assist(
change_return_type_to_result,
r#"fn foo() -<|>> i32 {
if true {
if false {
1 as i32
} else {
2 as i32
}
} else {
24 as i32
}
}"#,
r#"fn foo() -> Result<i32, ${0:_}> {
if true {
if false {
Ok(1 as i32)
} else {
Ok(2 as i32)
}
} else {
Ok(24 as i32)
}
}"#,
);
}
#[test]
fn change_return_type_to_result_simple_with_tail_block_like_match() {
check_assist(
change_return_type_to_result,
r#"fn foo() -> i32<|> {
let my_var = 5;
match my_var {
5 => 42i32,
_ => 24i32,
}
}"#,
r#"fn foo() -> Result<i32, ${0:_}> {
let my_var = 5;
match my_var {
5 => Ok(42i32),
_ => Ok(24i32),
}
}"#,
);
}
#[test]
fn change_return_type_to_result_simple_with_loop_with_tail() {
check_assist(
change_return_type_to_result,
r#"fn foo() -> i32<|> {
let my_var = 5;
loop {
println!("test");
5
}
my_var
}"#,
r#"fn foo() -> Result<i32, ${0:_}> {
let my_var = 5;
loop {
println!("test");
5
}
Ok(my_var)
}"#,
);
}
#[test]
fn change_return_type_to_result_simple_with_loop_in_let_stmt() {
check_assist(
change_return_type_to_result,
r#"fn foo() -> i32<|> {
let my_var = let x = loop {
break 1;
};
my_var
}"#,
r#"fn foo() -> Result<i32, ${0:_}> {
let my_var = let x = loop {
break 1;
};
Ok(my_var)
}"#,
);
}
#[test]
fn change_return_type_to_result_simple_with_tail_block_like_match_return_expr() {
check_assist(
change_return_type_to_result,
r#"fn foo() -> i32<|> {
let my_var = 5;
let res = match my_var {
5 => 42i32,
_ => return 24i32,
};
res
}"#,
r#"fn foo() -> Result<i32, ${0:_}> {
let my_var = 5;
let res = match my_var {
5 => 42i32,
_ => return Ok(24i32),
};
Ok(res)
}"#,
);
check_assist(
change_return_type_to_result,
r#"fn foo() -> i32<|> {
let my_var = 5;
let res = if my_var == 5 {
42i32
} else {
return 24i32;
};
res
}"#,
r#"fn foo() -> Result<i32, ${0:_}> {
let my_var = 5;
let res = if my_var == 5 {
42i32
} else {
return Ok(24i32);
};
Ok(res)
}"#,
);
}
#[test]
fn change_return_type_to_result_simple_with_tail_block_like_match_deeper() {
check_assist(
change_return_type_to_result,
r#"fn foo() -> i32<|> {
let my_var = 5;
match my_var {
5 => {
if true {
42i32
} else {
25i32
}
},
_ => {
let test = "test";
if test == "test" {
return bar();
}
53i32
},
}
}"#,
r#"fn foo() -> Result<i32, ${0:_}> {
let my_var = 5;
match my_var {
5 => {
if true {
Ok(42i32)
} else {
Ok(25i32)
}
},
_ => {
let test = "test";
if test == "test" {
return Ok(bar());
}
Ok(53i32)
},
}
}"#,
);
}
#[test]
fn change_return_type_to_result_simple_with_tail_block_like_early_return() {
check_assist(
change_return_type_to_result,
r#"fn foo() -> i<|>32 {
let test = "test";
if test == "test" {
return 24i32;
}
53i32
}"#,
r#"fn foo() -> Result<i32, ${0:_}> {
let test = "test";
if test == "test" {
return Ok(24i32);
}
Ok(53i32)
}"#,
);
}
#[test]
fn change_return_type_to_result_simple_with_closure() {
check_assist(
change_return_type_to_result,
r#"fn foo(the_field: u32) -><|> u32 {
let true_closure = || {
return true;
};
if the_field < 5 {
let mut i = 0;
if true_closure() {
return 99;
} else {
return 0;
}
}
the_field
}"#,
r#"fn foo(the_field: u32) -> Result<u32, ${0:_}> {
let true_closure = || {
return true;
};
if the_field < 5 {
let mut i = 0;
if true_closure() {
return Ok(99);
} else {
return Ok(0);
}
}
Ok(the_field)
}"#,
);
check_assist(
change_return_type_to_result,
r#"fn foo(the_field: u32) -> u32<|> {
let true_closure = || {
return true;
};
if the_field < 5 {
let mut i = 0;
if true_closure() {
return 99;
} else {
return 0;
}
}
let t = None;
t.unwrap_or_else(|| the_field)
}"#,
r#"fn foo(the_field: u32) -> Result<u32, ${0:_}> {
let true_closure = || {
return true;
};
if the_field < 5 {
let mut i = 0;
if true_closure() {
return Ok(99);
} else {
return Ok(0);
}
}
let t = None;
Ok(t.unwrap_or_else(|| the_field))
}"#,
);
}
#[test]
fn change_return_type_to_result_simple_with_weird_forms() {
check_assist(
change_return_type_to_result,
r#"fn foo() -> i32<|> {
let test = "test";
if test == "test" {
return 24i32;
}
let mut i = 0;
loop {
if i == 1 {
break 55;
}
i += 1;
}
}"#,
r#"fn foo() -> Result<i32, ${0:_}> {
let test = "test";
if test == "test" {
return Ok(24i32);
}
let mut i = 0;
loop {
if i == 1 {
break Ok(55);
}
i += 1;
}
}"#,
);
check_assist(
change_return_type_to_result,
r#"fn foo() -> i32<|> {
let test = "test";
if test == "test" {
return 24i32;
}
let mut i = 0;
loop {
loop {
if i == 1 {
break 55;
}
i += 1;
}
}
}"#,
r#"fn foo() -> Result<i32, ${0:_}> {
let test = "test";
if test == "test" {
return Ok(24i32);
}
let mut i = 0;
loop {
loop {
if i == 1 {
break Ok(55);
}
i += 1;
}
}
}"#,
);
check_assist(
change_return_type_to_result,
r#"fn foo() -> i3<|>2 {
let test = "test";
let other = 5;
if test == "test" {
let res = match other {
5 => 43,
_ => return 56,
};
}
let mut i = 0;
loop {
loop {
if i == 1 {
break 55;
}
i += 1;
}
}
}"#,
r#"fn foo() -> Result<i32, ${0:_}> {
let test = "test";
let other = 5;
if test == "test" {
let res = match other {
5 => 43,
_ => return Ok(56),
};
}
let mut i = 0;
loop {
loop {
if i == 1 {
break Ok(55);
}
i += 1;
}
}
}"#,
);
check_assist(
change_return_type_to_result,
r#"fn foo(the_field: u32) -> u32<|> {
if the_field < 5 {
let mut i = 0;
loop {
if i > 5 {
return 55u32;
}
i += 3;
}
match i {
5 => return 99,
_ => return 0,
};
}
the_field
}"#,
r#"fn foo(the_field: u32) -> Result<u32, ${0:_}> {
if the_field < 5 {
let mut i = 0;
loop {
if i > 5 {
return Ok(55u32);
}
i += 3;
}
match i {
5 => return Ok(99),
_ => return Ok(0),
};
}
Ok(the_field)
}"#,
);
check_assist(
change_return_type_to_result,
r#"fn foo(the_field: u32) -> u3<|>2 {
if the_field < 5 {
let mut i = 0;
match i {
5 => return 99,
_ => return 0,
}
}
the_field
}"#,
r#"fn foo(the_field: u32) -> Result<u32, ${0:_}> {
if the_field < 5 {
let mut i = 0;
match i {
5 => return Ok(99),
_ => return Ok(0),
}
}
Ok(the_field)
}"#,
);
check_assist(
change_return_type_to_result,
r#"fn foo(the_field: u32) -> u32<|> {
if the_field < 5 {
let mut i = 0;
if i == 5 {
return 99
} else {
return 0
}
}
the_field
}"#,
r#"fn foo(the_field: u32) -> Result<u32, ${0:_}> {
if the_field < 5 {
let mut i = 0;
if i == 5 {
return Ok(99)
} else {
return Ok(0)
}
}
Ok(the_field)
}"#,
);
check_assist(
change_return_type_to_result,
r#"fn foo(the_field: u32) -> <|>u32 {
if the_field < 5 {
let mut i = 0;
if i == 5 {
return 99;
} else {
return 0;
}
}
the_field
}"#,
r#"fn foo(the_field: u32) -> Result<u32, ${0:_}> {
if the_field < 5 {
let mut i = 0;
if i == 5 {
return Ok(99);
} else {
return Ok(0);
}
}
Ok(the_field)
}"#,
);
}
}

View file

@ -0,0 +1,200 @@
use syntax::{
ast::{self, NameOwner, VisibilityOwner},
AstNode,
SyntaxKind::{CONST, ENUM, FN, MODULE, STATIC, STRUCT, TRAIT, VISIBILITY},
T,
};
use test_utils::mark;
use crate::{utils::vis_offset, AssistContext, AssistId, AssistKind, Assists};
// Assist: change_visibility
//
// Adds or changes existing visibility specifier.
//
// ```
// <|>fn frobnicate() {}
// ```
// ->
// ```
// pub(crate) fn frobnicate() {}
// ```
pub(crate) fn change_visibility(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
if let Some(vis) = ctx.find_node_at_offset::<ast::Visibility>() {
return change_vis(acc, vis);
}
add_vis(acc, ctx)
}
fn add_vis(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
let item_keyword = ctx.token_at_offset().find(|leaf| {
matches!(
leaf.kind(),
T![const] | T![static] | T![fn] | T![mod] | T![struct] | T![enum] | T![trait]
)
});
let (offset, target) = if let Some(keyword) = item_keyword {
let parent = keyword.parent();
let def_kws = vec![CONST, STATIC, FN, MODULE, STRUCT, ENUM, TRAIT];
// Parent is not a definition, can't add visibility
if !def_kws.iter().any(|&def_kw| def_kw == parent.kind()) {
return None;
}
// Already have visibility, do nothing
if parent.children().any(|child| child.kind() == VISIBILITY) {
return None;
}
(vis_offset(&parent), keyword.text_range())
} else if let Some(field_name) = ctx.find_node_at_offset::<ast::Name>() {
let field = field_name.syntax().ancestors().find_map(ast::RecordField::cast)?;
if field.name()? != field_name {
mark::hit!(change_visibility_field_false_positive);
return None;
}
if field.visibility().is_some() {
return None;
}
(vis_offset(field.syntax()), field_name.syntax().text_range())
} else if let Some(field) = ctx.find_node_at_offset::<ast::TupleField>() {
if field.visibility().is_some() {
return None;
}
(vis_offset(field.syntax()), field.syntax().text_range())
} else {
return None;
};
acc.add(
AssistId("change_visibility", AssistKind::RefactorRewrite),
"Change visibility to pub(crate)",
target,
|edit| {
edit.insert(offset, "pub(crate) ");
},
)
}
fn change_vis(acc: &mut Assists, vis: ast::Visibility) -> Option<()> {
if vis.syntax().text() == "pub" {
let target = vis.syntax().text_range();
return acc.add(
AssistId("change_visibility", AssistKind::RefactorRewrite),
"Change Visibility to pub(crate)",
target,
|edit| {
edit.replace(vis.syntax().text_range(), "pub(crate)");
},
);
}
if vis.syntax().text() == "pub(crate)" {
let target = vis.syntax().text_range();
return acc.add(
AssistId("change_visibility", AssistKind::RefactorRewrite),
"Change visibility to pub",
target,
|edit| {
edit.replace(vis.syntax().text_range(), "pub");
},
);
}
None
}
#[cfg(test)]
mod tests {
use test_utils::mark;
use crate::tests::{check_assist, check_assist_not_applicable, check_assist_target};
use super::*;
#[test]
fn change_visibility_adds_pub_crate_to_items() {
check_assist(change_visibility, "<|>fn foo() {}", "pub(crate) fn foo() {}");
check_assist(change_visibility, "f<|>n foo() {}", "pub(crate) fn foo() {}");
check_assist(change_visibility, "<|>struct Foo {}", "pub(crate) struct Foo {}");
check_assist(change_visibility, "<|>mod foo {}", "pub(crate) mod foo {}");
check_assist(change_visibility, "<|>trait Foo {}", "pub(crate) trait Foo {}");
check_assist(change_visibility, "m<|>od {}", "pub(crate) mod {}");
check_assist(change_visibility, "unsafe f<|>n foo() {}", "pub(crate) unsafe fn foo() {}");
}
#[test]
fn change_visibility_works_with_struct_fields() {
check_assist(
change_visibility,
r"struct S { <|>field: u32 }",
r"struct S { pub(crate) field: u32 }",
);
check_assist(change_visibility, r"struct S ( <|>u32 )", r"struct S ( pub(crate) u32 )");
}
#[test]
fn change_visibility_field_false_positive() {
mark::check!(change_visibility_field_false_positive);
check_assist_not_applicable(
change_visibility,
r"struct S { field: [(); { let <|>x = ();}] }",
)
}
#[test]
fn change_visibility_pub_to_pub_crate() {
check_assist(change_visibility, "<|>pub fn foo() {}", "pub(crate) fn foo() {}")
}
#[test]
fn change_visibility_pub_crate_to_pub() {
check_assist(change_visibility, "<|>pub(crate) fn foo() {}", "pub fn foo() {}")
}
#[test]
fn change_visibility_const() {
check_assist(change_visibility, "<|>const FOO = 3u8;", "pub(crate) const FOO = 3u8;");
}
#[test]
fn change_visibility_static() {
check_assist(change_visibility, "<|>static FOO = 3u8;", "pub(crate) static FOO = 3u8;");
}
#[test]
fn change_visibility_handles_comment_attrs() {
check_assist(
change_visibility,
r"
/// docs
// comments
#[derive(Debug)]
<|>struct Foo;
",
r"
/// docs
// comments
#[derive(Debug)]
pub(crate) struct Foo;
",
)
}
#[test]
fn not_applicable_for_enum_variants() {
check_assist_not_applicable(
change_visibility,
r"mod foo { pub enum Foo {Foo1} }
fn main() { foo::Foo::Foo1<|> } ",
);
}
#[test]
fn change_visibility_target() {
check_assist_target(change_visibility, "<|>fn foo() {}", "fn");
check_assist_target(change_visibility, "pub(crate)<|> fn foo() {}", "pub(crate)");
check_assist_target(change_visibility, "struct S { <|>field: u32 }", "field");
}
}

View file

@ -0,0 +1,515 @@
use std::{iter::once, ops::RangeInclusive};
use syntax::{
algo::replace_children,
ast::{
self,
edit::{AstNodeEdit, IndentLevel},
make,
},
AstNode,
SyntaxKind::{FN, LOOP_EXPR, L_CURLY, R_CURLY, WHILE_EXPR, WHITESPACE},
SyntaxNode,
};
use crate::{
assist_context::{AssistContext, Assists},
utils::invert_boolean_expression,
AssistId, AssistKind,
};
// Assist: convert_to_guarded_return
//
// Replace a large conditional with a guarded return.
//
// ```
// fn main() {
// <|>if cond {
// foo();
// bar();
// }
// }
// ```
// ->
// ```
// fn main() {
// if !cond {
// return;
// }
// foo();
// bar();
// }
// ```
pub(crate) fn convert_to_guarded_return(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
let if_expr: ast::IfExpr = ctx.find_node_at_offset()?;
if if_expr.else_branch().is_some() {
return None;
}
let cond = if_expr.condition()?;
// Check if there is an IfLet that we can handle.
let if_let_pat = match cond.pat() {
None => None, // No IfLet, supported.
Some(ast::Pat::TupleStructPat(pat)) if pat.fields().count() == 1 => {
let path = pat.path()?;
match path.qualifier() {
None => {
let bound_ident = pat.fields().next().unwrap();
Some((path, bound_ident))
}
Some(_) => return None,
}
}
Some(_) => return None, // Unsupported IfLet.
};
let cond_expr = cond.expr()?;
let then_block = if_expr.then_branch()?;
let parent_block = if_expr.syntax().parent()?.ancestors().find_map(ast::BlockExpr::cast)?;
if parent_block.expr()? != if_expr.clone().into() {
return None;
}
// check for early return and continue
let first_in_then_block = then_block.syntax().first_child()?;
if ast::ReturnExpr::can_cast(first_in_then_block.kind())
|| ast::ContinueExpr::can_cast(first_in_then_block.kind())
|| first_in_then_block
.children()
.any(|x| ast::ReturnExpr::can_cast(x.kind()) || ast::ContinueExpr::can_cast(x.kind()))
{
return None;
}
let parent_container = parent_block.syntax().parent()?;
let early_expression: ast::Expr = match parent_container.kind() {
WHILE_EXPR | LOOP_EXPR => make::expr_continue(),
FN => make::expr_return(),
_ => return None,
};
if then_block.syntax().first_child_or_token().map(|t| t.kind() == L_CURLY).is_none() {
return None;
}
then_block.syntax().last_child_or_token().filter(|t| t.kind() == R_CURLY)?;
let target = if_expr.syntax().text_range();
acc.add(
AssistId("convert_to_guarded_return", AssistKind::RefactorRewrite),
"Convert to guarded return",
target,
|edit| {
let if_indent_level = IndentLevel::from_node(&if_expr.syntax());
let new_block = match if_let_pat {
None => {
// If.
let new_expr = {
let then_branch =
make::block_expr(once(make::expr_stmt(early_expression).into()), None);
let cond = invert_boolean_expression(cond_expr);
make::expr_if(make::condition(cond, None), then_branch)
.indent(if_indent_level)
};
replace(new_expr.syntax(), &then_block, &parent_block, &if_expr)
}
Some((path, bound_ident)) => {
// If-let.
let match_expr = {
let happy_arm = {
let pat = make::tuple_struct_pat(
path,
once(make::ident_pat(make::name("it")).into()),
);
let expr = {
let name_ref = make::name_ref("it");
let segment = make::path_segment(name_ref);
let path = make::path_unqualified(segment);
make::expr_path(path)
};
make::match_arm(once(pat.into()), expr)
};
let sad_arm = make::match_arm(
// FIXME: would be cool to use `None` or `Err(_)` if appropriate
once(make::wildcard_pat().into()),
early_expression,
);
make::expr_match(cond_expr, make::match_arm_list(vec![happy_arm, sad_arm]))
};
let let_stmt = make::let_stmt(
make::ident_pat(make::name(&bound_ident.syntax().to_string())).into(),
Some(match_expr),
);
let let_stmt = let_stmt.indent(if_indent_level);
replace(let_stmt.syntax(), &then_block, &parent_block, &if_expr)
}
};
edit.replace_ast(parent_block, ast::BlockExpr::cast(new_block).unwrap());
fn replace(
new_expr: &SyntaxNode,
then_block: &ast::BlockExpr,
parent_block: &ast::BlockExpr,
if_expr: &ast::IfExpr,
) -> SyntaxNode {
let then_block_items = then_block.dedent(IndentLevel(1));
let end_of_then = then_block_items.syntax().last_child_or_token().unwrap();
let end_of_then =
if end_of_then.prev_sibling_or_token().map(|n| n.kind()) == Some(WHITESPACE) {
end_of_then.prev_sibling_or_token().unwrap()
} else {
end_of_then
};
let mut then_statements = new_expr.children_with_tokens().chain(
then_block_items
.syntax()
.children_with_tokens()
.skip(1)
.take_while(|i| *i != end_of_then),
);
replace_children(
&parent_block.syntax(),
RangeInclusive::new(
if_expr.clone().syntax().clone().into(),
if_expr.syntax().clone().into(),
),
&mut then_statements,
)
}
},
)
}
#[cfg(test)]
mod tests {
use crate::tests::{check_assist, check_assist_not_applicable};
use super::*;
#[test]
fn convert_inside_fn() {
check_assist(
convert_to_guarded_return,
r#"
fn main() {
bar();
if<|> true {
foo();
//comment
bar();
}
}
"#,
r#"
fn main() {
bar();
if !true {
return;
}
foo();
//comment
bar();
}
"#,
);
}
#[test]
fn convert_let_inside_fn() {
check_assist(
convert_to_guarded_return,
r#"
fn main(n: Option<String>) {
bar();
if<|> let Some(n) = n {
foo(n);
//comment
bar();
}
}
"#,
r#"
fn main(n: Option<String>) {
bar();
let n = match n {
Some(it) => it,
_ => return,
};
foo(n);
//comment
bar();
}
"#,
);
}
#[test]
fn convert_if_let_result() {
check_assist(
convert_to_guarded_return,
r#"
fn main() {
if<|> let Ok(x) = Err(92) {
foo(x);
}
}
"#,
r#"
fn main() {
let x = match Err(92) {
Ok(it) => it,
_ => return,
};
foo(x);
}
"#,
);
}
#[test]
fn convert_let_ok_inside_fn() {
check_assist(
convert_to_guarded_return,
r#"
fn main(n: Option<String>) {
bar();
if<|> let Ok(n) = n {
foo(n);
//comment
bar();
}
}
"#,
r#"
fn main(n: Option<String>) {
bar();
let n = match n {
Ok(it) => it,
_ => return,
};
foo(n);
//comment
bar();
}
"#,
);
}
#[test]
fn convert_inside_while() {
check_assist(
convert_to_guarded_return,
r#"
fn main() {
while true {
if<|> true {
foo();
bar();
}
}
}
"#,
r#"
fn main() {
while true {
if !true {
continue;
}
foo();
bar();
}
}
"#,
);
}
#[test]
fn convert_let_inside_while() {
check_assist(
convert_to_guarded_return,
r#"
fn main() {
while true {
if<|> let Some(n) = n {
foo(n);
bar();
}
}
}
"#,
r#"
fn main() {
while true {
let n = match n {
Some(it) => it,
_ => continue,
};
foo(n);
bar();
}
}
"#,
);
}
#[test]
fn convert_inside_loop() {
check_assist(
convert_to_guarded_return,
r#"
fn main() {
loop {
if<|> true {
foo();
bar();
}
}
}
"#,
r#"
fn main() {
loop {
if !true {
continue;
}
foo();
bar();
}
}
"#,
);
}
#[test]
fn convert_let_inside_loop() {
check_assist(
convert_to_guarded_return,
r#"
fn main() {
loop {
if<|> let Some(n) = n {
foo(n);
bar();
}
}
}
"#,
r#"
fn main() {
loop {
let n = match n {
Some(it) => it,
_ => continue,
};
foo(n);
bar();
}
}
"#,
);
}
#[test]
fn ignore_already_converted_if() {
check_assist_not_applicable(
convert_to_guarded_return,
r#"
fn main() {
if<|> true {
return;
}
}
"#,
);
}
#[test]
fn ignore_already_converted_loop() {
check_assist_not_applicable(
convert_to_guarded_return,
r#"
fn main() {
loop {
if<|> true {
continue;
}
}
}
"#,
);
}
#[test]
fn ignore_return() {
check_assist_not_applicable(
convert_to_guarded_return,
r#"
fn main() {
if<|> true {
return
}
}
"#,
);
}
#[test]
fn ignore_else_branch() {
check_assist_not_applicable(
convert_to_guarded_return,
r#"
fn main() {
if<|> true {
foo();
} else {
bar()
}
}
"#,
);
}
#[test]
fn ignore_statements_aftert_if() {
check_assist_not_applicable(
convert_to_guarded_return,
r#"
fn main() {
if<|> true {
foo();
}
bar();
}
"#,
);
}
#[test]
fn ignore_statements_inside_if() {
check_assist_not_applicable(
convert_to_guarded_return,
r#"
fn main() {
if false {
if<|> true {
foo();
}
}
}
"#,
);
}
}

View file

@ -0,0 +1,385 @@
use either::Either;
use hir::{AssocItem, MacroDef, ModuleDef, Name, PathResolution, ScopeDef, SemanticsScope};
use ide_db::{
defs::{classify_name_ref, Definition, NameRefClass},
RootDatabase,
};
use syntax::{algo, ast, match_ast, AstNode, SyntaxNode, SyntaxToken, T};
use crate::{
assist_context::{AssistBuilder, AssistContext, Assists},
AssistId, AssistKind,
};
// Assist: expand_glob_import
//
// Expands glob imports.
//
// ```
// mod foo {
// pub struct Bar;
// pub struct Baz;
// }
//
// use foo::*<|>;
//
// fn qux(bar: Bar, baz: Baz) {}
// ```
// ->
// ```
// mod foo {
// pub struct Bar;
// pub struct Baz;
// }
//
// use foo::{Baz, Bar};
//
// fn qux(bar: Bar, baz: Baz) {}
// ```
pub(crate) fn expand_glob_import(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
let star = ctx.find_token_at_offset(T![*])?;
let mod_path = find_mod_path(&star)?;
let module = match ctx.sema.resolve_path(&mod_path)? {
PathResolution::Def(ModuleDef::Module(it)) => it,
_ => return None,
};
let source_file = ctx.source_file();
let scope = ctx.sema.scope_at_offset(source_file.syntax(), ctx.offset());
let defs_in_mod = find_defs_in_mod(ctx, scope, module)?;
let name_refs_in_source_file =
source_file.syntax().descendants().filter_map(ast::NameRef::cast).collect();
let used_names = find_used_names(ctx, defs_in_mod, name_refs_in_source_file);
let parent = star.parent().parent()?;
acc.add(
AssistId("expand_glob_import", AssistKind::RefactorRewrite),
"Expand glob import",
parent.text_range(),
|builder| {
replace_ast(builder, &parent, mod_path, used_names);
},
)
}
fn find_mod_path(star: &SyntaxToken) -> Option<ast::Path> {
star.ancestors().find_map(|n| ast::UseTree::cast(n).and_then(|u| u.path()))
}
#[derive(PartialEq)]
enum Def {
ModuleDef(ModuleDef),
MacroDef(MacroDef),
}
impl Def {
fn name(&self, db: &RootDatabase) -> Option<Name> {
match self {
Def::ModuleDef(def) => def.name(db),
Def::MacroDef(def) => def.name(db),
}
}
}
fn find_defs_in_mod(
ctx: &AssistContext,
from: SemanticsScope<'_>,
module: hir::Module,
) -> Option<Vec<Def>> {
let module_scope = module.scope(ctx.db(), from.module());
let mut defs = vec![];
for (_, def) in module_scope {
match def {
ScopeDef::ModuleDef(def) => defs.push(Def::ModuleDef(def)),
ScopeDef::MacroDef(def) => defs.push(Def::MacroDef(def)),
_ => continue,
}
}
Some(defs)
}
fn find_used_names(
ctx: &AssistContext,
defs_in_mod: Vec<Def>,
name_refs_in_source_file: Vec<ast::NameRef>,
) -> Vec<Name> {
let defs_in_source_file = name_refs_in_source_file
.iter()
.filter_map(|r| classify_name_ref(&ctx.sema, r))
.filter_map(|rc| match rc {
NameRefClass::Definition(Definition::ModuleDef(def)) => Some(Def::ModuleDef(def)),
NameRefClass::Definition(Definition::Macro(def)) => Some(Def::MacroDef(def)),
_ => None,
})
.collect::<Vec<Def>>();
defs_in_mod
.iter()
.filter(|def| {
if let Def::ModuleDef(ModuleDef::Trait(tr)) = def {
for item in tr.items(ctx.db()) {
if let AssocItem::Function(f) = item {
if defs_in_source_file.contains(&Def::ModuleDef(ModuleDef::Function(f))) {
return true;
}
}
}
}
defs_in_source_file.contains(def)
})
.filter_map(|d| d.name(ctx.db()))
.collect()
}
fn replace_ast(
builder: &mut AssistBuilder,
node: &SyntaxNode,
path: ast::Path,
used_names: Vec<Name>,
) {
let replacement: Either<ast::UseTree, ast::UseTreeList> = match used_names.as_slice() {
[name] => Either::Left(ast::make::use_tree(
ast::make::path_from_text(&format!("{}::{}", path, name)),
None,
None,
false,
)),
names => Either::Right(ast::make::use_tree_list(names.iter().map(|n| {
ast::make::use_tree(ast::make::path_from_text(&n.to_string()), None, None, false)
}))),
};
let mut replace_node = |replacement: Either<ast::UseTree, ast::UseTreeList>| {
algo::diff(node, &replacement.either(|u| u.syntax().clone(), |ut| ut.syntax().clone()))
.into_text_edit(builder.text_edit_builder());
};
match_ast! {
match node {
ast::UseTree(use_tree) => {
replace_node(replacement);
},
ast::UseTreeList(use_tree_list) => {
replace_node(replacement);
},
ast::Use(use_item) => {
builder.replace_ast(use_item, ast::make::use_(replacement.left_or_else(|ut| ast::make::use_tree(path, Some(ut), None, false))));
},
_ => {},
}
}
}
#[cfg(test)]
mod tests {
use crate::tests::{check_assist, check_assist_not_applicable};
use super::*;
#[test]
fn expanding_glob_import() {
check_assist(
expand_glob_import,
r"
mod foo {
pub struct Bar;
pub struct Baz;
pub struct Qux;
pub fn f() {}
}
use foo::*<|>;
fn qux(bar: Bar, baz: Baz) {
f();
}
",
r"
mod foo {
pub struct Bar;
pub struct Baz;
pub struct Qux;
pub fn f() {}
}
use foo::{Baz, Bar, f};
fn qux(bar: Bar, baz: Baz) {
f();
}
",
)
}
#[test]
fn expanding_glob_import_with_existing_explicit_names() {
check_assist(
expand_glob_import,
r"
mod foo {
pub struct Bar;
pub struct Baz;
pub struct Qux;
pub fn f() {}
}
use foo::{*<|>, f};
fn qux(bar: Bar, baz: Baz) {
f();
}
",
r"
mod foo {
pub struct Bar;
pub struct Baz;
pub struct Qux;
pub fn f() {}
}
use foo::{Baz, Bar, f};
fn qux(bar: Bar, baz: Baz) {
f();
}
",
)
}
#[test]
fn expanding_nested_glob_import() {
check_assist(
expand_glob_import,
r"
mod foo {
mod bar {
pub struct Bar;
pub struct Baz;
pub struct Qux;
pub fn f() {}
}
mod baz {
pub fn g() {}
}
}
use foo::{bar::{*<|>, f}, baz::*};
fn qux(bar: Bar, baz: Baz) {
f();
g();
}
",
r"
mod foo {
mod bar {
pub struct Bar;
pub struct Baz;
pub struct Qux;
pub fn f() {}
}
mod baz {
pub fn g() {}
}
}
use foo::{bar::{Baz, Bar, f}, baz::*};
fn qux(bar: Bar, baz: Baz) {
f();
g();
}
",
)
}
#[test]
fn expanding_glob_import_with_macro_defs() {
check_assist(
expand_glob_import,
r"
//- /lib.rs crate:foo
#[macro_export]
macro_rules! bar {
() => ()
}
pub fn baz() {}
//- /main.rs crate:main deps:foo
use foo::*<|>;
fn main() {
bar!();
baz();
}
",
r"
use foo::{bar, baz};
fn main() {
bar!();
baz();
}
",
)
}
#[test]
fn expanding_glob_import_with_trait_method_uses() {
check_assist(
expand_glob_import,
r"
//- /lib.rs crate:foo
pub trait Tr {
fn method(&self) {}
}
impl Tr for () {}
//- /main.rs crate:main deps:foo
use foo::*<|>;
fn main() {
().method();
}
",
r"
use foo::Tr;
fn main() {
().method();
}
",
)
}
#[test]
fn expanding_is_not_applicable_if_cursor_is_not_in_star_token() {
check_assist_not_applicable(
expand_glob_import,
r"
mod foo {
pub struct Bar;
pub struct Baz;
pub struct Qux;
}
use foo::Bar<|>;
fn qux(bar: Bar, baz: Baz) {}
",
)
}
}

View file

@ -0,0 +1,322 @@
use base_db::FileId;
use hir::{EnumVariant, Module, ModuleDef, Name};
use ide_db::{defs::Definition, search::Reference, RootDatabase};
use rustc_hash::FxHashSet;
use syntax::{
algo::find_node_at_offset,
ast::{self, edit::IndentLevel, ArgListOwner, AstNode, NameOwner, VisibilityOwner},
SourceFile, TextRange, TextSize,
};
use crate::{
assist_context::AssistBuilder, utils::insert_use_statement, AssistContext, AssistId,
AssistKind, Assists,
};
// Assist: extract_struct_from_enum_variant
//
// Extracts a struct from enum variant.
//
// ```
// enum A { <|>One(u32, u32) }
// ```
// ->
// ```
// struct One(pub u32, pub u32);
//
// enum A { One(One) }
// ```
pub(crate) fn extract_struct_from_enum_variant(
acc: &mut Assists,
ctx: &AssistContext,
) -> Option<()> {
let variant = ctx.find_node_at_offset::<ast::Variant>()?;
let field_list = match variant.kind() {
ast::StructKind::Tuple(field_list) => field_list,
_ => return None,
};
let variant_name = variant.name()?.to_string();
let variant_hir = ctx.sema.to_def(&variant)?;
if existing_struct_def(ctx.db(), &variant_name, &variant_hir) {
return None;
}
let enum_ast = variant.parent_enum();
let visibility = enum_ast.visibility();
let enum_hir = ctx.sema.to_def(&enum_ast)?;
let variant_hir_name = variant_hir.name(ctx.db());
let enum_module_def = ModuleDef::from(enum_hir);
let current_module = enum_hir.module(ctx.db());
let target = variant.syntax().text_range();
acc.add(
AssistId("extract_struct_from_enum_variant", AssistKind::RefactorRewrite),
"Extract struct from enum variant",
target,
|builder| {
let definition = Definition::ModuleDef(ModuleDef::EnumVariant(variant_hir));
let res = definition.usages(&ctx.sema).all();
let start_offset = variant.parent_enum().syntax().text_range().start();
let mut visited_modules_set = FxHashSet::default();
visited_modules_set.insert(current_module);
for reference in res {
let source_file = ctx.sema.parse(reference.file_range.file_id);
update_reference(
ctx,
builder,
reference,
&source_file,
&enum_module_def,
&variant_hir_name,
&mut visited_modules_set,
);
}
extract_struct_def(
builder,
&enum_ast,
&variant_name,
&field_list.to_string(),
start_offset,
ctx.frange.file_id,
&visibility,
);
let list_range = field_list.syntax().text_range();
update_variant(builder, &variant_name, ctx.frange.file_id, list_range);
},
)
}
fn existing_struct_def(db: &RootDatabase, variant_name: &str, variant: &EnumVariant) -> bool {
variant
.parent_enum(db)
.module(db)
.scope(db, None)
.into_iter()
.any(|(name, _)| name.to_string() == variant_name.to_string())
}
fn insert_import(
ctx: &AssistContext,
builder: &mut AssistBuilder,
path: &ast::PathExpr,
module: &Module,
enum_module_def: &ModuleDef,
variant_hir_name: &Name,
) -> Option<()> {
let db = ctx.db();
let mod_path = module.find_use_path(db, enum_module_def.clone());
if let Some(mut mod_path) = mod_path {
mod_path.segments.pop();
mod_path.segments.push(variant_hir_name.clone());
insert_use_statement(
path.syntax(),
&mod_path.to_string(),
ctx,
builder.text_edit_builder(),
);
}
Some(())
}
// FIXME: this should use strongly-typed `make`, rather than string manipulation.
fn extract_struct_def(
builder: &mut AssistBuilder,
enum_: &ast::Enum,
variant_name: &str,
variant_list: &str,
start_offset: TextSize,
file_id: FileId,
visibility: &Option<ast::Visibility>,
) -> Option<()> {
let visibility_string = if let Some(visibility) = visibility {
format!("{} ", visibility.to_string())
} else {
"".to_string()
};
let indent = IndentLevel::from_node(enum_.syntax());
let struct_def = format!(
r#"{}struct {}{};
{}"#,
visibility_string,
variant_name,
list_with_visibility(variant_list),
indent
);
builder.edit_file(file_id);
builder.insert(start_offset, struct_def);
Some(())
}
fn update_variant(
builder: &mut AssistBuilder,
variant_name: &str,
file_id: FileId,
list_range: TextRange,
) -> Option<()> {
let inside_variant_range = TextRange::new(
list_range.start().checked_add(TextSize::from(1))?,
list_range.end().checked_sub(TextSize::from(1))?,
);
builder.edit_file(file_id);
builder.replace(inside_variant_range, variant_name);
Some(())
}
fn update_reference(
ctx: &AssistContext,
builder: &mut AssistBuilder,
reference: Reference,
source_file: &SourceFile,
enum_module_def: &ModuleDef,
variant_hir_name: &Name,
visited_modules_set: &mut FxHashSet<Module>,
) -> Option<()> {
let path_expr: ast::PathExpr = find_node_at_offset::<ast::PathExpr>(
source_file.syntax(),
reference.file_range.range.start(),
)?;
let call = path_expr.syntax().parent().and_then(ast::CallExpr::cast)?;
let list = call.arg_list()?;
let segment = path_expr.path()?.segment()?;
let module = ctx.sema.scope(&path_expr.syntax()).module()?;
let list_range = list.syntax().text_range();
let inside_list_range = TextRange::new(
list_range.start().checked_add(TextSize::from(1))?,
list_range.end().checked_sub(TextSize::from(1))?,
);
builder.edit_file(reference.file_range.file_id);
if !visited_modules_set.contains(&module) {
if insert_import(ctx, builder, &path_expr, &module, enum_module_def, variant_hir_name)
.is_some()
{
visited_modules_set.insert(module);
}
}
builder.replace(inside_list_range, format!("{}{}", segment, list));
Some(())
}
fn list_with_visibility(list: &str) -> String {
list.split(',')
.map(|part| {
let index = if part.chars().next().unwrap() == '(' { 1usize } else { 0 };
let mut mod_part = part.trim().to_string();
mod_part.insert_str(index, "pub ");
mod_part
})
.collect::<Vec<String>>()
.join(", ")
}
#[cfg(test)]
mod tests {
use crate::{
tests::{check_assist, check_assist_not_applicable},
utils::FamousDefs,
};
use super::*;
#[test]
fn test_extract_struct_several_fields() {
check_assist(
extract_struct_from_enum_variant,
"enum A { <|>One(u32, u32) }",
r#"struct One(pub u32, pub u32);
enum A { One(One) }"#,
);
}
#[test]
fn test_extract_struct_one_field() {
check_assist(
extract_struct_from_enum_variant,
"enum A { <|>One(u32) }",
r#"struct One(pub u32);
enum A { One(One) }"#,
);
}
#[test]
fn test_extract_struct_pub_visibility() {
check_assist(
extract_struct_from_enum_variant,
"pub enum A { <|>One(u32, u32) }",
r#"pub struct One(pub u32, pub u32);
pub enum A { One(One) }"#,
);
}
#[test]
fn test_extract_struct_with_complex_imports() {
check_assist(
extract_struct_from_enum_variant,
r#"mod my_mod {
fn another_fn() {
let m = my_other_mod::MyEnum::MyField(1, 1);
}
pub mod my_other_mod {
fn another_fn() {
let m = MyEnum::MyField(1, 1);
}
pub enum MyEnum {
<|>MyField(u8, u8),
}
}
}
fn another_fn() {
let m = my_mod::my_other_mod::MyEnum::MyField(1, 1);
}"#,
r#"use my_mod::my_other_mod::MyField;
mod my_mod {
use my_other_mod::MyField;
fn another_fn() {
let m = my_other_mod::MyEnum::MyField(MyField(1, 1));
}
pub mod my_other_mod {
fn another_fn() {
let m = MyEnum::MyField(MyField(1, 1));
}
pub struct MyField(pub u8, pub u8);
pub enum MyEnum {
MyField(MyField),
}
}
}
fn another_fn() {
let m = my_mod::my_other_mod::MyEnum::MyField(MyField(1, 1));
}"#,
);
}
fn check_not_applicable(ra_fixture: &str) {
let fixture =
format!("//- /main.rs crate:main deps:core\n{}\n{}", ra_fixture, FamousDefs::FIXTURE);
check_assist_not_applicable(extract_struct_from_enum_variant, &fixture)
}
#[test]
fn test_extract_enum_not_applicable_for_element_with_no_fields() {
check_not_applicable("enum A { <|>One }");
}
#[test]
fn test_extract_enum_not_applicable_if_struct_exists() {
check_not_applicable(
r#"struct One;
enum A { <|>One(u8) }"#,
);
}
}

View file

@ -0,0 +1,588 @@
use stdx::format_to;
use syntax::{
ast::{self, AstNode},
SyntaxKind::{
BLOCK_EXPR, BREAK_EXPR, CLOSURE_EXPR, COMMENT, LOOP_EXPR, MATCH_ARM, PATH_EXPR, RETURN_EXPR,
},
SyntaxNode,
};
use test_utils::mark;
use crate::{AssistContext, AssistId, AssistKind, Assists};
// Assist: extract_variable
//
// Extracts subexpression into a variable.
//
// ```
// fn main() {
// <|>(1 + 2)<|> * 4;
// }
// ```
// ->
// ```
// fn main() {
// let $0var_name = (1 + 2);
// var_name * 4;
// }
// ```
pub(crate) fn extract_variable(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
if ctx.frange.range.is_empty() {
return None;
}
let node = ctx.covering_element();
if node.kind() == COMMENT {
mark::hit!(extract_var_in_comment_is_not_applicable);
return None;
}
let to_extract = node.ancestors().find_map(valid_target_expr)?;
let anchor = Anchor::from(&to_extract)?;
let indent = anchor.syntax().prev_sibling_or_token()?.as_token()?.clone();
let target = to_extract.syntax().text_range();
acc.add(
AssistId("extract_variable", AssistKind::RefactorExtract),
"Extract into variable",
target,
move |edit| {
let field_shorthand =
match to_extract.syntax().parent().and_then(ast::RecordExprField::cast) {
Some(field) => field.name_ref(),
None => None,
};
let mut buf = String::new();
let var_name = match &field_shorthand {
Some(it) => it.to_string(),
None => "var_name".to_string(),
};
let expr_range = match &field_shorthand {
Some(it) => it.syntax().text_range().cover(to_extract.syntax().text_range()),
None => to_extract.syntax().text_range(),
};
if let Anchor::WrapInBlock(_) = anchor {
format_to!(buf, "{{ let {} = ", var_name);
} else {
format_to!(buf, "let {} = ", var_name);
};
format_to!(buf, "{}", to_extract.syntax());
if let Anchor::Replace(stmt) = anchor {
mark::hit!(test_extract_var_expr_stmt);
if stmt.semicolon_token().is_none() {
buf.push_str(";");
}
match ctx.config.snippet_cap {
Some(cap) => {
let snip = buf
.replace(&format!("let {}", var_name), &format!("let $0{}", var_name));
edit.replace_snippet(cap, expr_range, snip)
}
None => edit.replace(expr_range, buf),
}
return;
}
buf.push_str(";");
// We want to maintain the indent level,
// but we do not want to duplicate possible
// extra newlines in the indent block
let text = indent.text();
if text.starts_with('\n') {
buf.push_str("\n");
buf.push_str(text.trim_start_matches('\n'));
} else {
buf.push_str(text);
}
edit.replace(expr_range, var_name.clone());
let offset = anchor.syntax().text_range().start();
match ctx.config.snippet_cap {
Some(cap) => {
let snip =
buf.replace(&format!("let {}", var_name), &format!("let $0{}", var_name));
edit.insert_snippet(cap, offset, snip)
}
None => edit.insert(offset, buf),
}
if let Anchor::WrapInBlock(_) = anchor {
edit.insert(anchor.syntax().text_range().end(), " }");
}
},
)
}
/// Check whether the node is a valid expression which can be extracted to a variable.
/// In general that's true for any expression, but in some cases that would produce invalid code.
fn valid_target_expr(node: SyntaxNode) -> Option<ast::Expr> {
match node.kind() {
PATH_EXPR | LOOP_EXPR => None,
BREAK_EXPR => ast::BreakExpr::cast(node).and_then(|e| e.expr()),
RETURN_EXPR => ast::ReturnExpr::cast(node).and_then(|e| e.expr()),
BLOCK_EXPR => {
ast::BlockExpr::cast(node).filter(|it| it.is_standalone()).map(ast::Expr::from)
}
_ => ast::Expr::cast(node),
}
}
enum Anchor {
Before(SyntaxNode),
Replace(ast::ExprStmt),
WrapInBlock(SyntaxNode),
}
impl Anchor {
fn from(to_extract: &ast::Expr) -> Option<Anchor> {
to_extract.syntax().ancestors().find_map(|node| {
if let Some(expr) =
node.parent().and_then(ast::BlockExpr::cast).and_then(|it| it.expr())
{
if expr.syntax() == &node {
mark::hit!(test_extract_var_last_expr);
return Some(Anchor::Before(node));
}
}
if let Some(parent) = node.parent() {
if parent.kind() == MATCH_ARM || parent.kind() == CLOSURE_EXPR {
return Some(Anchor::WrapInBlock(node));
}
}
if let Some(stmt) = ast::Stmt::cast(node.clone()) {
if let ast::Stmt::ExprStmt(stmt) = stmt {
if stmt.expr().as_ref() == Some(to_extract) {
return Some(Anchor::Replace(stmt));
}
}
return Some(Anchor::Before(node));
}
None
})
}
fn syntax(&self) -> &SyntaxNode {
match self {
Anchor::Before(it) | Anchor::WrapInBlock(it) => it,
Anchor::Replace(stmt) => stmt.syntax(),
}
}
}
#[cfg(test)]
mod tests {
use test_utils::mark;
use crate::tests::{check_assist, check_assist_not_applicable, check_assist_target};
use super::*;
#[test]
fn test_extract_var_simple() {
check_assist(
extract_variable,
r#"
fn foo() {
foo(<|>1 + 1<|>);
}"#,
r#"
fn foo() {
let $0var_name = 1 + 1;
foo(var_name);
}"#,
);
}
#[test]
fn extract_var_in_comment_is_not_applicable() {
mark::check!(extract_var_in_comment_is_not_applicable);
check_assist_not_applicable(extract_variable, "fn main() { 1 + /* <|>comment<|> */ 1; }");
}
#[test]
fn test_extract_var_expr_stmt() {
mark::check!(test_extract_var_expr_stmt);
check_assist(
extract_variable,
r#"
fn foo() {
<|>1 + 1<|>;
}"#,
r#"
fn foo() {
let $0var_name = 1 + 1;
}"#,
);
check_assist(
extract_variable,
"
fn foo() {
<|>{ let x = 0; x }<|>
something_else();
}",
"
fn foo() {
let $0var_name = { let x = 0; x };
something_else();
}",
);
}
#[test]
fn test_extract_var_part_of_expr_stmt() {
check_assist(
extract_variable,
"
fn foo() {
<|>1<|> + 1;
}",
"
fn foo() {
let $0var_name = 1;
var_name + 1;
}",
);
}
#[test]
fn test_extract_var_last_expr() {
mark::check!(test_extract_var_last_expr);
check_assist(
extract_variable,
r#"
fn foo() {
bar(<|>1 + 1<|>)
}
"#,
r#"
fn foo() {
let $0var_name = 1 + 1;
bar(var_name)
}
"#,
);
check_assist(
extract_variable,
r#"
fn foo() {
<|>bar(1 + 1)<|>
}
"#,
r#"
fn foo() {
let $0var_name = bar(1 + 1);
var_name
}
"#,
)
}
#[test]
fn test_extract_var_in_match_arm_no_block() {
check_assist(
extract_variable,
"
fn main() {
let x = true;
let tuple = match x {
true => (<|>2 + 2<|>, true)
_ => (0, false)
};
}
",
"
fn main() {
let x = true;
let tuple = match x {
true => { let $0var_name = 2 + 2; (var_name, true) }
_ => (0, false)
};
}
",
);
}
#[test]
fn test_extract_var_in_match_arm_with_block() {
check_assist(
extract_variable,
"
fn main() {
let x = true;
let tuple = match x {
true => {
let y = 1;
(<|>2 + y<|>, true)
}
_ => (0, false)
};
}
",
"
fn main() {
let x = true;
let tuple = match x {
true => {
let y = 1;
let $0var_name = 2 + y;
(var_name, true)
}
_ => (0, false)
};
}
",
);
}
#[test]
fn test_extract_var_in_closure_no_block() {
check_assist(
extract_variable,
"
fn main() {
let lambda = |x: u32| <|>x * 2<|>;
}
",
"
fn main() {
let lambda = |x: u32| { let $0var_name = x * 2; var_name };
}
",
);
}
#[test]
fn test_extract_var_in_closure_with_block() {
check_assist(
extract_variable,
"
fn main() {
let lambda = |x: u32| { <|>x * 2<|> };
}
",
"
fn main() {
let lambda = |x: u32| { let $0var_name = x * 2; var_name };
}
",
);
}
#[test]
fn test_extract_var_path_simple() {
check_assist(
extract_variable,
"
fn main() {
let o = <|>Some(true)<|>;
}
",
"
fn main() {
let $0var_name = Some(true);
let o = var_name;
}
",
);
}
#[test]
fn test_extract_var_path_method() {
check_assist(
extract_variable,
"
fn main() {
let v = <|>bar.foo()<|>;
}
",
"
fn main() {
let $0var_name = bar.foo();
let v = var_name;
}
",
);
}
#[test]
fn test_extract_var_return() {
check_assist(
extract_variable,
"
fn foo() -> u32 {
<|>return 2 + 2<|>;
}
",
"
fn foo() -> u32 {
let $0var_name = 2 + 2;
return var_name;
}
",
);
}
#[test]
fn test_extract_var_does_not_add_extra_whitespace() {
check_assist(
extract_variable,
"
fn foo() -> u32 {
<|>return 2 + 2<|>;
}
",
"
fn foo() -> u32 {
let $0var_name = 2 + 2;
return var_name;
}
",
);
check_assist(
extract_variable,
"
fn foo() -> u32 {
<|>return 2 + 2<|>;
}
",
"
fn foo() -> u32 {
let $0var_name = 2 + 2;
return var_name;
}
",
);
check_assist(
extract_variable,
"
fn foo() -> u32 {
let foo = 1;
// bar
<|>return 2 + 2<|>;
}
",
"
fn foo() -> u32 {
let foo = 1;
// bar
let $0var_name = 2 + 2;
return var_name;
}
",
);
}
#[test]
fn test_extract_var_break() {
check_assist(
extract_variable,
"
fn main() {
let result = loop {
<|>break 2 + 2<|>;
};
}
",
"
fn main() {
let result = loop {
let $0var_name = 2 + 2;
break var_name;
};
}
",
);
}
#[test]
fn test_extract_var_for_cast() {
check_assist(
extract_variable,
"
fn main() {
let v = <|>0f32 as u32<|>;
}
",
"
fn main() {
let $0var_name = 0f32 as u32;
let v = var_name;
}
",
);
}
#[test]
fn extract_var_field_shorthand() {
check_assist(
extract_variable,
r#"
struct S {
foo: i32
}
fn main() {
S { foo: <|>1 + 1<|> }
}
"#,
r#"
struct S {
foo: i32
}
fn main() {
let $0foo = 1 + 1;
S { foo }
}
"#,
)
}
#[test]
fn test_extract_var_for_return_not_applicable() {
check_assist_not_applicable(extract_variable, "fn foo() { <|>return<|>; } ");
}
#[test]
fn test_extract_var_for_break_not_applicable() {
check_assist_not_applicable(extract_variable, "fn main() { loop { <|>break<|>; }; }");
}
// FIXME: This is not quite correct, but good enough(tm) for the sorting heuristic
#[test]
fn extract_var_target() {
check_assist_target(extract_variable, "fn foo() -> u32 { <|>return 2 + 2<|>; }", "2 + 2");
check_assist_target(
extract_variable,
"
fn main() {
let x = true;
let tuple = match x {
true => (<|>2 + 2<|>, true)
_ => (0, false)
};
}
",
"2 + 2",
);
}
}

View file

@ -0,0 +1,747 @@
use std::iter;
use hir::{Adt, HasSource, ModuleDef, Semantics};
use ide_db::RootDatabase;
use itertools::Itertools;
use syntax::ast::{self, make, AstNode, MatchArm, NameOwner, Pat};
use test_utils::mark;
use crate::{
utils::{render_snippet, Cursor, FamousDefs},
AssistContext, AssistId, AssistKind, Assists,
};
// Assist: fill_match_arms
//
// Adds missing clauses to a `match` expression.
//
// ```
// enum Action { Move { distance: u32 }, Stop }
//
// fn handle(action: Action) {
// match action {
// <|>
// }
// }
// ```
// ->
// ```
// enum Action { Move { distance: u32 }, Stop }
//
// fn handle(action: Action) {
// match action {
// $0Action::Move { distance } => {}
// Action::Stop => {}
// }
// }
// ```
pub(crate) fn fill_match_arms(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
let match_expr = ctx.find_node_at_offset::<ast::MatchExpr>()?;
let match_arm_list = match_expr.match_arm_list()?;
let expr = match_expr.expr()?;
let mut arms: Vec<MatchArm> = match_arm_list.arms().collect();
if arms.len() == 1 {
if let Some(Pat::WildcardPat(..)) = arms[0].pat() {
arms.clear();
}
}
let module = ctx.sema.scope(expr.syntax()).module()?;
let missing_arms: Vec<MatchArm> = if let Some(enum_def) = resolve_enum_def(&ctx.sema, &expr) {
let variants = enum_def.variants(ctx.db());
let mut variants = variants
.into_iter()
.filter_map(|variant| build_pat(ctx.db(), module, variant))
.filter(|variant_pat| is_variant_missing(&mut arms, variant_pat))
.map(|pat| make::match_arm(iter::once(pat), make::expr_empty_block()))
.collect::<Vec<_>>();
if Some(enum_def) == FamousDefs(&ctx.sema, module.krate()).core_option_Option() {
// Match `Some` variant first.
mark::hit!(option_order);
variants.reverse()
}
variants
} else if let Some(enum_defs) = resolve_tuple_of_enum_def(&ctx.sema, &expr) {
// Partial fill not currently supported for tuple of enums.
if !arms.is_empty() {
return None;
}
// We do not currently support filling match arms for a tuple
// containing a single enum.
if enum_defs.len() < 2 {
return None;
}
// When calculating the match arms for a tuple of enums, we want
// to create a match arm for each possible combination of enum
// values. The `multi_cartesian_product` method transforms
// Vec<Vec<EnumVariant>> into Vec<(EnumVariant, .., EnumVariant)>
// where each tuple represents a proposed match arm.
enum_defs
.into_iter()
.map(|enum_def| enum_def.variants(ctx.db()))
.multi_cartesian_product()
.map(|variants| {
let patterns =
variants.into_iter().filter_map(|variant| build_pat(ctx.db(), module, variant));
ast::Pat::from(make::tuple_pat(patterns))
})
.filter(|variant_pat| is_variant_missing(&mut arms, variant_pat))
.map(|pat| make::match_arm(iter::once(pat), make::expr_empty_block()))
.collect()
} else {
return None;
};
if missing_arms.is_empty() {
return None;
}
let target = match_expr.syntax().text_range();
acc.add(
AssistId("fill_match_arms", AssistKind::QuickFix),
"Fill match arms",
target,
|builder| {
let new_arm_list = match_arm_list.remove_placeholder();
let n_old_arms = new_arm_list.arms().count();
let new_arm_list = new_arm_list.append_arms(missing_arms);
let first_new_arm = new_arm_list.arms().nth(n_old_arms);
let old_range = match_arm_list.syntax().text_range();
match (first_new_arm, ctx.config.snippet_cap) {
(Some(first_new_arm), Some(cap)) => {
let extend_lifetime;
let cursor =
match first_new_arm.syntax().descendants().find_map(ast::WildcardPat::cast)
{
Some(it) => {
extend_lifetime = it.syntax().clone();
Cursor::Replace(&extend_lifetime)
}
None => Cursor::Before(first_new_arm.syntax()),
};
let snippet = render_snippet(cap, new_arm_list.syntax(), cursor);
builder.replace_snippet(cap, old_range, snippet);
}
_ => builder.replace(old_range, new_arm_list.to_string()),
}
},
)
}
fn is_variant_missing(existing_arms: &mut Vec<MatchArm>, var: &Pat) -> bool {
existing_arms.iter().filter_map(|arm| arm.pat()).all(|pat| {
// Special casee OrPat as separate top-level pats
let top_level_pats: Vec<Pat> = match pat {
Pat::OrPat(pats) => pats.pats().collect::<Vec<_>>(),
_ => vec![pat],
};
!top_level_pats.iter().any(|pat| does_pat_match_variant(pat, var))
})
}
fn does_pat_match_variant(pat: &Pat, var: &Pat) -> bool {
let first_node_text = |pat: &Pat| pat.syntax().first_child().map(|node| node.text());
let pat_head = match pat {
Pat::IdentPat(bind_pat) => {
if let Some(p) = bind_pat.pat() {
first_node_text(&p)
} else {
return false;
}
}
pat => first_node_text(pat),
};
let var_head = first_node_text(var);
pat_head == var_head
}
fn resolve_enum_def(sema: &Semantics<RootDatabase>, expr: &ast::Expr) -> Option<hir::Enum> {
sema.type_of_expr(&expr)?.autoderef(sema.db).find_map(|ty| match ty.as_adt() {
Some(Adt::Enum(e)) => Some(e),
_ => None,
})
}
fn resolve_tuple_of_enum_def(
sema: &Semantics<RootDatabase>,
expr: &ast::Expr,
) -> Option<Vec<hir::Enum>> {
sema.type_of_expr(&expr)?
.tuple_fields(sema.db)
.iter()
.map(|ty| {
ty.autoderef(sema.db).find_map(|ty| match ty.as_adt() {
Some(Adt::Enum(e)) => Some(e),
// For now we only handle expansion for a tuple of enums. Here
// we map non-enum items to None and rely on `collect` to
// convert Vec<Option<hir::Enum>> into Option<Vec<hir::Enum>>.
_ => None,
})
})
.collect()
}
fn build_pat(db: &RootDatabase, module: hir::Module, var: hir::EnumVariant) -> Option<ast::Pat> {
let path = crate::ast_transform::path_to_ast(module.find_use_path(db, ModuleDef::from(var))?);
// FIXME: use HIR for this; it doesn't currently expose struct vs. tuple vs. unit variants though
let pat: ast::Pat = match var.source(db).value.kind() {
ast::StructKind::Tuple(field_list) => {
let pats = iter::repeat(make::wildcard_pat().into()).take(field_list.fields().count());
make::tuple_struct_pat(path, pats).into()
}
ast::StructKind::Record(field_list) => {
let pats = field_list.fields().map(|f| make::ident_pat(f.name().unwrap()).into());
make::record_pat(path, pats).into()
}
ast::StructKind::Unit => make::path_pat(path),
};
Some(pat)
}
#[cfg(test)]
mod tests {
use test_utils::mark;
use crate::{
tests::{check_assist, check_assist_not_applicable, check_assist_target},
utils::FamousDefs,
};
use super::fill_match_arms;
#[test]
fn all_match_arms_provided() {
check_assist_not_applicable(
fill_match_arms,
r#"
enum A {
As,
Bs{x:i32, y:Option<i32>},
Cs(i32, Option<i32>),
}
fn main() {
match A::As<|> {
A::As,
A::Bs{x,y:Some(_)} => {}
A::Cs(_, Some(_)) => {}
}
}
"#,
);
}
#[test]
fn tuple_of_non_enum() {
// for now this case is not handled, although it potentially could be
// in the future
check_assist_not_applicable(
fill_match_arms,
r#"
fn main() {
match (0, false)<|> {
}
}
"#,
);
}
#[test]
fn partial_fill_record_tuple() {
check_assist(
fill_match_arms,
r#"
enum A {
As,
Bs { x: i32, y: Option<i32> },
Cs(i32, Option<i32>),
}
fn main() {
match A::As<|> {
A::Bs { x, y: Some(_) } => {}
A::Cs(_, Some(_)) => {}
}
}
"#,
r#"
enum A {
As,
Bs { x: i32, y: Option<i32> },
Cs(i32, Option<i32>),
}
fn main() {
match A::As {
A::Bs { x, y: Some(_) } => {}
A::Cs(_, Some(_)) => {}
$0A::As => {}
}
}
"#,
);
}
#[test]
fn partial_fill_or_pat() {
check_assist(
fill_match_arms,
r#"
enum A { As, Bs, Cs(Option<i32>) }
fn main() {
match A::As<|> {
A::Cs(_) | A::Bs => {}
}
}
"#,
r#"
enum A { As, Bs, Cs(Option<i32>) }
fn main() {
match A::As {
A::Cs(_) | A::Bs => {}
$0A::As => {}
}
}
"#,
);
}
#[test]
fn partial_fill() {
check_assist(
fill_match_arms,
r#"
enum A { As, Bs, Cs, Ds(String), Es(B) }
enum B { Xs, Ys }
fn main() {
match A::As<|> {
A::Bs if 0 < 1 => {}
A::Ds(_value) => { let x = 1; }
A::Es(B::Xs) => (),
}
}
"#,
r#"
enum A { As, Bs, Cs, Ds(String), Es(B) }
enum B { Xs, Ys }
fn main() {
match A::As {
A::Bs if 0 < 1 => {}
A::Ds(_value) => { let x = 1; }
A::Es(B::Xs) => (),
$0A::As => {}
A::Cs => {}
}
}
"#,
);
}
#[test]
fn partial_fill_bind_pat() {
check_assist(
fill_match_arms,
r#"
enum A { As, Bs, Cs(Option<i32>) }
fn main() {
match A::As<|> {
A::As(_) => {}
a @ A::Bs(_) => {}
}
}
"#,
r#"
enum A { As, Bs, Cs(Option<i32>) }
fn main() {
match A::As {
A::As(_) => {}
a @ A::Bs(_) => {}
A::Cs(${0:_}) => {}
}
}
"#,
);
}
#[test]
fn fill_match_arms_empty_body() {
check_assist(
fill_match_arms,
r#"
enum A { As, Bs, Cs(String), Ds(String, String), Es { x: usize, y: usize } }
fn main() {
let a = A::As;
match a<|> {}
}
"#,
r#"
enum A { As, Bs, Cs(String), Ds(String, String), Es { x: usize, y: usize } }
fn main() {
let a = A::As;
match a {
$0A::As => {}
A::Bs => {}
A::Cs(_) => {}
A::Ds(_, _) => {}
A::Es { x, y } => {}
}
}
"#,
);
}
#[test]
fn fill_match_arms_tuple_of_enum() {
check_assist(
fill_match_arms,
r#"
enum A { One, Two }
enum B { One, Two }
fn main() {
let a = A::One;
let b = B::One;
match (a<|>, b) {}
}
"#,
r#"
enum A { One, Two }
enum B { One, Two }
fn main() {
let a = A::One;
let b = B::One;
match (a, b) {
$0(A::One, B::One) => {}
(A::One, B::Two) => {}
(A::Two, B::One) => {}
(A::Two, B::Two) => {}
}
}
"#,
);
}
#[test]
fn fill_match_arms_tuple_of_enum_ref() {
check_assist(
fill_match_arms,
r#"
enum A { One, Two }
enum B { One, Two }
fn main() {
let a = A::One;
let b = B::One;
match (&a<|>, &b) {}
}
"#,
r#"
enum A { One, Two }
enum B { One, Two }
fn main() {
let a = A::One;
let b = B::One;
match (&a, &b) {
$0(A::One, B::One) => {}
(A::One, B::Two) => {}
(A::Two, B::One) => {}
(A::Two, B::Two) => {}
}
}
"#,
);
}
#[test]
fn fill_match_arms_tuple_of_enum_partial() {
check_assist_not_applicable(
fill_match_arms,
r#"
enum A { One, Two }
enum B { One, Two }
fn main() {
let a = A::One;
let b = B::One;
match (a<|>, b) {
(A::Two, B::One) => {}
}
}
"#,
);
}
#[test]
fn fill_match_arms_tuple_of_enum_not_applicable() {
check_assist_not_applicable(
fill_match_arms,
r#"
enum A { One, Two }
enum B { One, Two }
fn main() {
let a = A::One;
let b = B::One;
match (a<|>, b) {
(A::Two, B::One) => {}
(A::One, B::One) => {}
(A::One, B::Two) => {}
(A::Two, B::Two) => {}
}
}
"#,
);
}
#[test]
fn fill_match_arms_single_element_tuple_of_enum() {
// For now we don't hande the case of a single element tuple, but
// we could handle this in the future if `make::tuple_pat` allowed
// creating a tuple with a single pattern.
check_assist_not_applicable(
fill_match_arms,
r#"
enum A { One, Two }
fn main() {
let a = A::One;
match (a<|>, ) {
}
}
"#,
);
}
#[test]
fn test_fill_match_arm_refs() {
check_assist(
fill_match_arms,
r#"
enum A { As }
fn foo(a: &A) {
match a<|> {
}
}
"#,
r#"
enum A { As }
fn foo(a: &A) {
match a {
$0A::As => {}
}
}
"#,
);
check_assist(
fill_match_arms,
r#"
enum A {
Es { x: usize, y: usize }
}
fn foo(a: &mut A) {
match a<|> {
}
}
"#,
r#"
enum A {
Es { x: usize, y: usize }
}
fn foo(a: &mut A) {
match a {
$0A::Es { x, y } => {}
}
}
"#,
);
}
#[test]
fn fill_match_arms_target() {
check_assist_target(
fill_match_arms,
r#"
enum E { X, Y }
fn main() {
match E::X<|> {}
}
"#,
"match E::X {}",
);
}
#[test]
fn fill_match_arms_trivial_arm() {
check_assist(
fill_match_arms,
r#"
enum E { X, Y }
fn main() {
match E::X {
<|>_ => {}
}
}
"#,
r#"
enum E { X, Y }
fn main() {
match E::X {
$0E::X => {}
E::Y => {}
}
}
"#,
);
}
#[test]
fn fill_match_arms_qualifies_path() {
check_assist(
fill_match_arms,
r#"
mod foo { pub enum E { X, Y } }
use foo::E::X;
fn main() {
match X {
<|>
}
}
"#,
r#"
mod foo { pub enum E { X, Y } }
use foo::E::X;
fn main() {
match X {
$0X => {}
foo::E::Y => {}
}
}
"#,
);
}
#[test]
fn fill_match_arms_preserves_comments() {
check_assist(
fill_match_arms,
r#"
enum A { One, Two }
fn foo(a: A) {
match a {
// foo bar baz<|>
A::One => {}
// This is where the rest should be
}
}
"#,
r#"
enum A { One, Two }
fn foo(a: A) {
match a {
// foo bar baz
A::One => {}
// This is where the rest should be
$0A::Two => {}
}
}
"#,
);
}
#[test]
fn fill_match_arms_preserves_comments_empty() {
check_assist(
fill_match_arms,
r#"
enum A { One, Two }
fn foo(a: A) {
match a {
// foo bar baz<|>
}
}
"#,
r#"
enum A { One, Two }
fn foo(a: A) {
match a {
// foo bar baz
$0A::One => {}
A::Two => {}
}
}
"#,
);
}
#[test]
fn fill_match_arms_placeholder() {
check_assist(
fill_match_arms,
r#"
enum A { One, Two, }
fn foo(a: A) {
match a<|> {
_ => (),
}
}
"#,
r#"
enum A { One, Two, }
fn foo(a: A) {
match a {
$0A::One => {}
A::Two => {}
}
}
"#,
);
}
#[test]
fn option_order() {
mark::check!(option_order);
let before = r#"
fn foo(opt: Option<i32>) {
match opt<|> {
}
}
"#;
let before = &format!("//- /main.rs crate:main deps:core{}{}", before, FamousDefs::FIXTURE);
check_assist(
fill_match_arms,
before,
r#"
fn foo(opt: Option<i32>) {
match opt {
Some(${0:_}) => {}
None => {}
}
}
"#,
);
}
}

View file

@ -0,0 +1,607 @@
use base_db::FileId;
use hir::{db::HirDatabase, HasSource, HasVisibility, PathResolution};
use syntax::{ast, AstNode, TextRange, TextSize};
use crate::{utils::vis_offset, AssistContext, AssistId, AssistKind, Assists};
use ast::VisibilityOwner;
// FIXME: this really should be a fix for diagnostic, rather than an assist.
// Assist: fix_visibility
//
// Makes inaccessible item public.
//
// ```
// mod m {
// fn frobnicate() {}
// }
// fn main() {
// m::frobnicate<|>() {}
// }
// ```
// ->
// ```
// mod m {
// $0pub(crate) fn frobnicate() {}
// }
// fn main() {
// m::frobnicate() {}
// }
// ```
pub(crate) fn fix_visibility(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
add_vis_to_referenced_module_def(acc, ctx)
.or_else(|| add_vis_to_referenced_record_field(acc, ctx))
}
fn add_vis_to_referenced_module_def(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
let path: ast::Path = ctx.find_node_at_offset()?;
let path_res = ctx.sema.resolve_path(&path)?;
let def = match path_res {
PathResolution::Def(def) => def,
_ => return None,
};
let current_module = ctx.sema.scope(&path.syntax()).module()?;
let target_module = def.module(ctx.db())?;
let vis = target_module.visibility_of(ctx.db(), &def)?;
if vis.is_visible_from(ctx.db(), current_module.into()) {
return None;
};
let (offset, current_visibility, target, target_file, target_name) =
target_data_for_def(ctx.db(), def)?;
let missing_visibility =
if current_module.krate() == target_module.krate() { "pub(crate)" } else { "pub" };
let assist_label = match target_name {
None => format!("Change visibility to {}", missing_visibility),
Some(name) => format!("Change visibility of {} to {}", name, missing_visibility),
};
acc.add(AssistId("fix_visibility", AssistKind::QuickFix), assist_label, target, |builder| {
builder.edit_file(target_file);
match ctx.config.snippet_cap {
Some(cap) => match current_visibility {
Some(current_visibility) => builder.replace_snippet(
cap,
current_visibility.syntax().text_range(),
format!("$0{}", missing_visibility),
),
None => builder.insert_snippet(cap, offset, format!("$0{} ", missing_visibility)),
},
None => match current_visibility {
Some(current_visibility) => {
builder.replace(current_visibility.syntax().text_range(), missing_visibility)
}
None => builder.insert(offset, format!("{} ", missing_visibility)),
},
}
})
}
fn add_vis_to_referenced_record_field(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
let record_field: ast::RecordExprField = ctx.find_node_at_offset()?;
let (record_field_def, _) = ctx.sema.resolve_record_field(&record_field)?;
let current_module = ctx.sema.scope(record_field.syntax()).module()?;
let visibility = record_field_def.visibility(ctx.db());
if visibility.is_visible_from(ctx.db(), current_module.into()) {
return None;
}
let parent = record_field_def.parent_def(ctx.db());
let parent_name = parent.name(ctx.db());
let target_module = parent.module(ctx.db());
let in_file_source = record_field_def.source(ctx.db());
let (offset, current_visibility, target) = match in_file_source.value {
hir::FieldSource::Named(it) => {
let s = it.syntax();
(vis_offset(s), it.visibility(), s.text_range())
}
hir::FieldSource::Pos(it) => {
let s = it.syntax();
(vis_offset(s), it.visibility(), s.text_range())
}
};
let missing_visibility =
if current_module.krate() == target_module.krate() { "pub(crate)" } else { "pub" };
let target_file = in_file_source.file_id.original_file(ctx.db());
let target_name = record_field_def.name(ctx.db());
let assist_label =
format!("Change visibility of {}.{} to {}", parent_name, target_name, missing_visibility);
acc.add(AssistId("fix_visibility", AssistKind::QuickFix), assist_label, target, |builder| {
builder.edit_file(target_file);
match ctx.config.snippet_cap {
Some(cap) => match current_visibility {
Some(current_visibility) => builder.replace_snippet(
cap,
current_visibility.syntax().text_range(),
format!("$0{}", missing_visibility),
),
None => builder.insert_snippet(cap, offset, format!("$0{} ", missing_visibility)),
},
None => match current_visibility {
Some(current_visibility) => {
builder.replace(current_visibility.syntax().text_range(), missing_visibility)
}
None => builder.insert(offset, format!("{} ", missing_visibility)),
},
}
})
}
fn target_data_for_def(
db: &dyn HirDatabase,
def: hir::ModuleDef,
) -> Option<(TextSize, Option<ast::Visibility>, TextRange, FileId, Option<hir::Name>)> {
fn offset_target_and_file_id<S, Ast>(
db: &dyn HirDatabase,
x: S,
) -> (TextSize, Option<ast::Visibility>, TextRange, FileId)
where
S: HasSource<Ast = Ast>,
Ast: AstNode + ast::VisibilityOwner,
{
let source = x.source(db);
let in_file_syntax = source.syntax();
let file_id = in_file_syntax.file_id;
let syntax = in_file_syntax.value;
let current_visibility = source.value.visibility();
(
vis_offset(syntax),
current_visibility,
syntax.text_range(),
file_id.original_file(db.upcast()),
)
}
let target_name;
let (offset, current_visibility, target, target_file) = match def {
hir::ModuleDef::Function(f) => {
target_name = Some(f.name(db));
offset_target_and_file_id(db, f)
}
hir::ModuleDef::Adt(adt) => {
target_name = Some(adt.name(db));
match adt {
hir::Adt::Struct(s) => offset_target_and_file_id(db, s),
hir::Adt::Union(u) => offset_target_and_file_id(db, u),
hir::Adt::Enum(e) => offset_target_and_file_id(db, e),
}
}
hir::ModuleDef::Const(c) => {
target_name = c.name(db);
offset_target_and_file_id(db, c)
}
hir::ModuleDef::Static(s) => {
target_name = s.name(db);
offset_target_and_file_id(db, s)
}
hir::ModuleDef::Trait(t) => {
target_name = Some(t.name(db));
offset_target_and_file_id(db, t)
}
hir::ModuleDef::TypeAlias(t) => {
target_name = Some(t.name(db));
offset_target_and_file_id(db, t)
}
hir::ModuleDef::Module(m) => {
target_name = m.name(db);
let in_file_source = m.declaration_source(db)?;
let file_id = in_file_source.file_id.original_file(db.upcast());
let syntax = in_file_source.value.syntax();
(vis_offset(syntax), in_file_source.value.visibility(), syntax.text_range(), file_id)
}
// Enum variants can't be private, we can't modify builtin types
hir::ModuleDef::EnumVariant(_) | hir::ModuleDef::BuiltinType(_) => return None,
};
Some((offset, current_visibility, target, target_file, target_name))
}
#[cfg(test)]
mod tests {
use crate::tests::{check_assist, check_assist_not_applicable};
use super::*;
#[test]
fn fix_visibility_of_fn() {
check_assist(
fix_visibility,
r"mod foo { fn foo() {} }
fn main() { foo::foo<|>() } ",
r"mod foo { $0pub(crate) fn foo() {} }
fn main() { foo::foo() } ",
);
check_assist_not_applicable(
fix_visibility,
r"mod foo { pub fn foo() {} }
fn main() { foo::foo<|>() } ",
)
}
#[test]
fn fix_visibility_of_adt_in_submodule() {
check_assist(
fix_visibility,
r"mod foo { struct Foo; }
fn main() { foo::Foo<|> } ",
r"mod foo { $0pub(crate) struct Foo; }
fn main() { foo::Foo } ",
);
check_assist_not_applicable(
fix_visibility,
r"mod foo { pub struct Foo; }
fn main() { foo::Foo<|> } ",
);
check_assist(
fix_visibility,
r"mod foo { enum Foo; }
fn main() { foo::Foo<|> } ",
r"mod foo { $0pub(crate) enum Foo; }
fn main() { foo::Foo } ",
);
check_assist_not_applicable(
fix_visibility,
r"mod foo { pub enum Foo; }
fn main() { foo::Foo<|> } ",
);
check_assist(
fix_visibility,
r"mod foo { union Foo; }
fn main() { foo::Foo<|> } ",
r"mod foo { $0pub(crate) union Foo; }
fn main() { foo::Foo } ",
);
check_assist_not_applicable(
fix_visibility,
r"mod foo { pub union Foo; }
fn main() { foo::Foo<|> } ",
);
}
#[test]
fn fix_visibility_of_adt_in_other_file() {
check_assist(
fix_visibility,
r"
//- /main.rs
mod foo;
fn main() { foo::Foo<|> }
//- /foo.rs
struct Foo;
",
r"$0pub(crate) struct Foo;
",
);
}
#[test]
fn fix_visibility_of_struct_field() {
check_assist(
fix_visibility,
r"mod foo { pub struct Foo { bar: (), } }
fn main() { foo::Foo { <|>bar: () }; } ",
r"mod foo { pub struct Foo { $0pub(crate) bar: (), } }
fn main() { foo::Foo { bar: () }; } ",
);
check_assist(
fix_visibility,
r"
//- /lib.rs
mod foo;
fn main() { foo::Foo { <|>bar: () }; }
//- /foo.rs
pub struct Foo { bar: () }
",
r"pub struct Foo { $0pub(crate) bar: () }
",
);
check_assist_not_applicable(
fix_visibility,
r"mod foo { pub struct Foo { pub bar: (), } }
fn main() { foo::Foo { <|>bar: () }; } ",
);
check_assist_not_applicable(
fix_visibility,
r"
//- /lib.rs
mod foo;
fn main() { foo::Foo { <|>bar: () }; }
//- /foo.rs
pub struct Foo { pub bar: () }
",
);
}
#[test]
fn fix_visibility_of_enum_variant_field() {
check_assist(
fix_visibility,
r"mod foo { pub enum Foo { Bar { bar: () } } }
fn main() { foo::Foo::Bar { <|>bar: () }; } ",
r"mod foo { pub enum Foo { Bar { $0pub(crate) bar: () } } }
fn main() { foo::Foo::Bar { bar: () }; } ",
);
check_assist(
fix_visibility,
r"
//- /lib.rs
mod foo;
fn main() { foo::Foo::Bar { <|>bar: () }; }
//- /foo.rs
pub enum Foo { Bar { bar: () } }
",
r"pub enum Foo { Bar { $0pub(crate) bar: () } }
",
);
check_assist_not_applicable(
fix_visibility,
r"mod foo { pub struct Foo { pub bar: (), } }
fn main() { foo::Foo { <|>bar: () }; } ",
);
check_assist_not_applicable(
fix_visibility,
r"
//- /lib.rs
mod foo;
fn main() { foo::Foo { <|>bar: () }; }
//- /foo.rs
pub struct Foo { pub bar: () }
",
);
}
#[test]
#[ignore]
// FIXME reenable this test when `Semantics::resolve_record_field` works with union fields
fn fix_visibility_of_union_field() {
check_assist(
fix_visibility,
r"mod foo { pub union Foo { bar: (), } }
fn main() { foo::Foo { <|>bar: () }; } ",
r"mod foo { pub union Foo { $0pub(crate) bar: (), } }
fn main() { foo::Foo { bar: () }; } ",
);
check_assist(
fix_visibility,
r"
//- /lib.rs
mod foo;
fn main() { foo::Foo { <|>bar: () }; }
//- /foo.rs
pub union Foo { bar: () }
",
r"pub union Foo { $0pub(crate) bar: () }
",
);
check_assist_not_applicable(
fix_visibility,
r"mod foo { pub union Foo { pub bar: (), } }
fn main() { foo::Foo { <|>bar: () }; } ",
);
check_assist_not_applicable(
fix_visibility,
r"
//- /lib.rs
mod foo;
fn main() { foo::Foo { <|>bar: () }; }
//- /foo.rs
pub union Foo { pub bar: () }
",
);
}
#[test]
fn fix_visibility_of_const() {
check_assist(
fix_visibility,
r"mod foo { const FOO: () = (); }
fn main() { foo::FOO<|> } ",
r"mod foo { $0pub(crate) const FOO: () = (); }
fn main() { foo::FOO } ",
);
check_assist_not_applicable(
fix_visibility,
r"mod foo { pub const FOO: () = (); }
fn main() { foo::FOO<|> } ",
);
}
#[test]
fn fix_visibility_of_static() {
check_assist(
fix_visibility,
r"mod foo { static FOO: () = (); }
fn main() { foo::FOO<|> } ",
r"mod foo { $0pub(crate) static FOO: () = (); }
fn main() { foo::FOO } ",
);
check_assist_not_applicable(
fix_visibility,
r"mod foo { pub static FOO: () = (); }
fn main() { foo::FOO<|> } ",
);
}
#[test]
fn fix_visibility_of_trait() {
check_assist(
fix_visibility,
r"mod foo { trait Foo { fn foo(&self) {} } }
fn main() { let x: &dyn foo::<|>Foo; } ",
r"mod foo { $0pub(crate) trait Foo { fn foo(&self) {} } }
fn main() { let x: &dyn foo::Foo; } ",
);
check_assist_not_applicable(
fix_visibility,
r"mod foo { pub trait Foo { fn foo(&self) {} } }
fn main() { let x: &dyn foo::Foo<|>; } ",
);
}
#[test]
fn fix_visibility_of_type_alias() {
check_assist(
fix_visibility,
r"mod foo { type Foo = (); }
fn main() { let x: foo::Foo<|>; } ",
r"mod foo { $0pub(crate) type Foo = (); }
fn main() { let x: foo::Foo; } ",
);
check_assist_not_applicable(
fix_visibility,
r"mod foo { pub type Foo = (); }
fn main() { let x: foo::Foo<|>; } ",
);
}
#[test]
fn fix_visibility_of_module() {
check_assist(
fix_visibility,
r"mod foo { mod bar { fn bar() {} } }
fn main() { foo::bar<|>::bar(); } ",
r"mod foo { $0pub(crate) mod bar { fn bar() {} } }
fn main() { foo::bar::bar(); } ",
);
check_assist(
fix_visibility,
r"
//- /main.rs
mod foo;
fn main() { foo::bar<|>::baz(); }
//- /foo.rs
mod bar {
pub fn baz() {}
}
",
r"$0pub(crate) mod bar {
pub fn baz() {}
}
",
);
check_assist_not_applicable(
fix_visibility,
r"mod foo { pub mod bar { pub fn bar() {} } }
fn main() { foo::bar<|>::bar(); } ",
);
}
#[test]
fn fix_visibility_of_inline_module_in_other_file() {
check_assist(
fix_visibility,
r"
//- /main.rs
mod foo;
fn main() { foo::bar<|>::baz(); }
//- /foo.rs
mod bar;
//- /foo/bar.rs
pub fn baz() {}
",
r"$0pub(crate) mod bar;
",
);
}
#[test]
fn fix_visibility_of_module_declaration_in_other_file() {
check_assist(
fix_visibility,
r"
//- /main.rs
mod foo;
fn main() { foo::bar<|>>::baz(); }
//- /foo.rs
mod bar {
pub fn baz() {}
}
",
r"$0pub(crate) mod bar {
pub fn baz() {}
}
",
);
}
#[test]
fn adds_pub_when_target_is_in_another_crate() {
check_assist(
fix_visibility,
r"
//- /main.rs crate:a deps:foo
foo::Bar<|>
//- /lib.rs crate:foo
struct Bar;
",
r"$0pub struct Bar;
",
)
}
#[test]
fn replaces_pub_crate_with_pub() {
check_assist(
fix_visibility,
r"
//- /main.rs crate:a deps:foo
foo::Bar<|>
//- /lib.rs crate:foo
pub(crate) struct Bar;
",
r"$0pub struct Bar;
",
);
check_assist(
fix_visibility,
r"
//- /main.rs crate:a deps:foo
fn main() {
foo::Foo { <|>bar: () };
}
//- /lib.rs crate:foo
pub struct Foo { pub(crate) bar: () }
",
r"pub struct Foo { $0pub bar: () }
",
);
}
#[test]
#[ignore]
// FIXME handle reexports properly
fn fix_visibility_of_reexport() {
check_assist(
fix_visibility,
r"
mod foo {
use bar::Baz;
mod bar { pub(super) struct Baz; }
}
foo::Baz<|>
",
r"
mod foo {
$0pub(crate) use bar::Baz;
mod bar { pub(super) struct Baz; }
}
foo::Baz
",
)
}
}

View file

@ -0,0 +1,142 @@
use syntax::ast::{AstNode, BinExpr, BinOp};
use crate::{AssistContext, AssistId, AssistKind, Assists};
// Assist: flip_binexpr
//
// Flips operands of a binary expression.
//
// ```
// fn main() {
// let _ = 90 +<|> 2;
// }
// ```
// ->
// ```
// fn main() {
// let _ = 2 + 90;
// }
// ```
pub(crate) fn flip_binexpr(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
let expr = ctx.find_node_at_offset::<BinExpr>()?;
let lhs = expr.lhs()?.syntax().clone();
let rhs = expr.rhs()?.syntax().clone();
let op_range = expr.op_token()?.text_range();
// The assist should be applied only if the cursor is on the operator
let cursor_in_range = op_range.contains_range(ctx.frange.range);
if !cursor_in_range {
return None;
}
let action: FlipAction = expr.op_kind()?.into();
// The assist should not be applied for certain operators
if let FlipAction::DontFlip = action {
return None;
}
acc.add(
AssistId("flip_binexpr", AssistKind::RefactorRewrite),
"Flip binary expression",
op_range,
|edit| {
if let FlipAction::FlipAndReplaceOp(new_op) = action {
edit.replace(op_range, new_op);
}
edit.replace(lhs.text_range(), rhs.text());
edit.replace(rhs.text_range(), lhs.text());
},
)
}
enum FlipAction {
// Flip the expression
Flip,
// Flip the expression and replace the operator with this string
FlipAndReplaceOp(&'static str),
// Do not flip the expression
DontFlip,
}
impl From<BinOp> for FlipAction {
fn from(op_kind: BinOp) -> Self {
match op_kind {
kind if kind.is_assignment() => FlipAction::DontFlip,
BinOp::GreaterTest => FlipAction::FlipAndReplaceOp("<"),
BinOp::GreaterEqualTest => FlipAction::FlipAndReplaceOp("<="),
BinOp::LesserTest => FlipAction::FlipAndReplaceOp(">"),
BinOp::LesserEqualTest => FlipAction::FlipAndReplaceOp(">="),
_ => FlipAction::Flip,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tests::{check_assist, check_assist_not_applicable, check_assist_target};
#[test]
fn flip_binexpr_target_is_the_op() {
check_assist_target(flip_binexpr, "fn f() { let res = 1 ==<|> 2; }", "==")
}
#[test]
fn flip_binexpr_not_applicable_for_assignment() {
check_assist_not_applicable(flip_binexpr, "fn f() { let mut _x = 1; _x +=<|> 2 }")
}
#[test]
fn flip_binexpr_works_for_eq() {
check_assist(
flip_binexpr,
"fn f() { let res = 1 ==<|> 2; }",
"fn f() { let res = 2 == 1; }",
)
}
#[test]
fn flip_binexpr_works_for_gt() {
check_assist(flip_binexpr, "fn f() { let res = 1 ><|> 2; }", "fn f() { let res = 2 < 1; }")
}
#[test]
fn flip_binexpr_works_for_lteq() {
check_assist(
flip_binexpr,
"fn f() { let res = 1 <=<|> 2; }",
"fn f() { let res = 2 >= 1; }",
)
}
#[test]
fn flip_binexpr_works_for_complex_expr() {
check_assist(
flip_binexpr,
"fn f() { let res = (1 + 1) ==<|> (2 + 2); }",
"fn f() { let res = (2 + 2) == (1 + 1); }",
)
}
#[test]
fn flip_binexpr_works_inside_match() {
check_assist(
flip_binexpr,
r#"
fn dyn_eq(&self, other: &dyn Diagnostic) -> bool {
match other.downcast_ref::<Self>() {
None => false,
Some(it) => it ==<|> self,
}
}
"#,
r#"
fn dyn_eq(&self, other: &dyn Diagnostic) -> bool {
match other.downcast_ref::<Self>() {
None => false,
Some(it) => self == it,
}
}
"#,
)
}
}

View file

@ -0,0 +1,84 @@
use syntax::{algo::non_trivia_sibling, Direction, T};
use crate::{AssistContext, AssistId, AssistKind, Assists};
// Assist: flip_comma
//
// Flips two comma-separated items.
//
// ```
// fn main() {
// ((1, 2),<|> (3, 4));
// }
// ```
// ->
// ```
// fn main() {
// ((3, 4), (1, 2));
// }
// ```
pub(crate) fn flip_comma(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
let comma = ctx.find_token_at_offset(T![,])?;
let prev = non_trivia_sibling(comma.clone().into(), Direction::Prev)?;
let next = non_trivia_sibling(comma.clone().into(), Direction::Next)?;
// Don't apply a "flip" in case of a last comma
// that typically comes before punctuation
if next.kind().is_punct() {
return None;
}
acc.add(
AssistId("flip_comma", AssistKind::RefactorRewrite),
"Flip comma",
comma.text_range(),
|edit| {
edit.replace(prev.text_range(), next.to_string());
edit.replace(next.text_range(), prev.to_string());
},
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tests::{check_assist, check_assist_target};
#[test]
fn flip_comma_works_for_function_parameters() {
check_assist(
flip_comma,
"fn foo(x: i32,<|> y: Result<(), ()>) {}",
"fn foo(y: Result<(), ()>, x: i32) {}",
)
}
#[test]
fn flip_comma_target() {
check_assist_target(flip_comma, "fn foo(x: i32,<|> y: Result<(), ()>) {}", ",")
}
#[test]
#[should_panic]
fn flip_comma_before_punct() {
// See https://github.com/rust-analyzer/rust-analyzer/issues/1619
// "Flip comma" assist shouldn't be applicable to the last comma in enum or struct
// declaration body.
check_assist_target(
flip_comma,
"pub enum Test { \
A,<|> \
}",
",",
);
check_assist_target(
flip_comma,
"pub struct Test { \
foo: usize,<|> \
}",
",",
);
}
}

View file

@ -0,0 +1,121 @@
use syntax::{
algo::non_trivia_sibling,
ast::{self, AstNode},
Direction, T,
};
use crate::{AssistContext, AssistId, AssistKind, Assists};
// Assist: flip_trait_bound
//
// Flips two trait bounds.
//
// ```
// fn foo<T: Clone +<|> Copy>() { }
// ```
// ->
// ```
// fn foo<T: Copy + Clone>() { }
// ```
pub(crate) fn flip_trait_bound(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
// We want to replicate the behavior of `flip_binexpr` by only suggesting
// the assist when the cursor is on a `+`
let plus = ctx.find_token_at_offset(T![+])?;
// Make sure we're in a `TypeBoundList`
if ast::TypeBoundList::cast(plus.parent()).is_none() {
return None;
}
let (before, after) = (
non_trivia_sibling(plus.clone().into(), Direction::Prev)?,
non_trivia_sibling(plus.clone().into(), Direction::Next)?,
);
let target = plus.text_range();
acc.add(
AssistId("flip_trait_bound", AssistKind::RefactorRewrite),
"Flip trait bounds",
target,
|edit| {
edit.replace(before.text_range(), after.to_string());
edit.replace(after.text_range(), before.to_string());
},
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tests::{check_assist, check_assist_not_applicable, check_assist_target};
#[test]
fn flip_trait_bound_assist_available() {
check_assist_target(flip_trait_bound, "struct S<T> where T: A <|>+ B + C { }", "+")
}
#[test]
fn flip_trait_bound_not_applicable_for_single_trait_bound() {
check_assist_not_applicable(flip_trait_bound, "struct S<T> where T: <|>A { }")
}
#[test]
fn flip_trait_bound_works_for_struct() {
check_assist(
flip_trait_bound,
"struct S<T> where T: A <|>+ B { }",
"struct S<T> where T: B + A { }",
)
}
#[test]
fn flip_trait_bound_works_for_trait_impl() {
check_assist(
flip_trait_bound,
"impl X for S<T> where T: A +<|> B { }",
"impl X for S<T> where T: B + A { }",
)
}
#[test]
fn flip_trait_bound_works_for_fn() {
check_assist(flip_trait_bound, "fn f<T: A <|>+ B>(t: T) { }", "fn f<T: B + A>(t: T) { }")
}
#[test]
fn flip_trait_bound_works_for_fn_where_clause() {
check_assist(
flip_trait_bound,
"fn f<T>(t: T) where T: A +<|> B { }",
"fn f<T>(t: T) where T: B + A { }",
)
}
#[test]
fn flip_trait_bound_works_for_lifetime() {
check_assist(
flip_trait_bound,
"fn f<T>(t: T) where T: A <|>+ 'static { }",
"fn f<T>(t: T) where T: 'static + A { }",
)
}
#[test]
fn flip_trait_bound_works_for_complex_bounds() {
check_assist(
flip_trait_bound,
"struct S<T> where T: A<T> <|>+ b_mod::B<T> + C<T> { }",
"struct S<T> where T: b_mod::B<T> + A<T> + C<T> { }",
)
}
#[test]
fn flip_trait_bound_works_for_long_bounds() {
check_assist(
flip_trait_bound,
"struct S<T> where T: A + B + C + D + E + F +<|> G + H + I + J { }",
"struct S<T> where T: A + B + C + D + E + G + F + H + I + J { }",
)
}
}

View file

@ -0,0 +1,132 @@
use syntax::{
ast::{self, AstNode, AttrsOwner},
SyntaxKind::{COMMENT, WHITESPACE},
TextSize,
};
use crate::{AssistContext, AssistId, AssistKind, Assists};
// Assist: generate_derive
//
// Adds a new `#[derive()]` clause to a struct or enum.
//
// ```
// struct Point {
// x: u32,
// y: u32,<|>
// }
// ```
// ->
// ```
// #[derive($0)]
// struct Point {
// x: u32,
// y: u32,
// }
// ```
pub(crate) fn generate_derive(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
let cap = ctx.config.snippet_cap?;
let nominal = ctx.find_node_at_offset::<ast::AdtDef>()?;
let node_start = derive_insertion_offset(&nominal)?;
let target = nominal.syntax().text_range();
acc.add(
AssistId("generate_derive", AssistKind::Generate),
"Add `#[derive]`",
target,
|builder| {
let derive_attr = nominal
.attrs()
.filter_map(|x| x.as_simple_call())
.filter(|(name, _arg)| name == "derive")
.map(|(_name, arg)| arg)
.next();
match derive_attr {
None => {
builder.insert_snippet(cap, node_start, "#[derive($0)]\n");
}
Some(tt) => {
// Just move the cursor.
builder.insert_snippet(
cap,
tt.syntax().text_range().end() - TextSize::of(')'),
"$0",
)
}
};
},
)
}
// Insert `derive` after doc comments.
fn derive_insertion_offset(nominal: &ast::AdtDef) -> Option<TextSize> {
let non_ws_child = nominal
.syntax()
.children_with_tokens()
.find(|it| it.kind() != COMMENT && it.kind() != WHITESPACE)?;
Some(non_ws_child.text_range().start())
}
#[cfg(test)]
mod tests {
use crate::tests::{check_assist, check_assist_target};
use super::*;
#[test]
fn add_derive_new() {
check_assist(
generate_derive,
"struct Foo { a: i32, <|>}",
"#[derive($0)]\nstruct Foo { a: i32, }",
);
check_assist(
generate_derive,
"struct Foo { <|> a: i32, }",
"#[derive($0)]\nstruct Foo { a: i32, }",
);
}
#[test]
fn add_derive_existing() {
check_assist(
generate_derive,
"#[derive(Clone)]\nstruct Foo { a: i32<|>, }",
"#[derive(Clone$0)]\nstruct Foo { a: i32, }",
);
}
#[test]
fn add_derive_new_with_doc_comment() {
check_assist(
generate_derive,
"
/// `Foo` is a pretty important struct.
/// It does stuff.
struct Foo { a: i32<|>, }
",
"
/// `Foo` is a pretty important struct.
/// It does stuff.
#[derive($0)]
struct Foo { a: i32, }
",
);
}
#[test]
fn add_derive_target() {
check_assist_target(
generate_derive,
"
struct SomeThingIrrelevant;
/// `Foo` is a pretty important struct.
/// It does stuff.
struct Foo { a: i32<|>, }
struct EvenMoreIrrelevant;
",
"/// `Foo` is a pretty important struct.
/// It does stuff.
struct Foo { a: i32, }",
);
}
}

View file

@ -0,0 +1,200 @@
use ide_db::RootDatabase;
use syntax::ast::{self, AstNode, NameOwner};
use test_utils::mark;
use crate::{utils::FamousDefs, AssistContext, AssistId, AssistKind, Assists};
// Assist: generate_from_impl_for_enum
//
// Adds a From impl for an enum variant with one tuple field.
//
// ```
// enum A { <|>One(u32) }
// ```
// ->
// ```
// enum A { One(u32) }
//
// impl From<u32> for A {
// fn from(v: u32) -> Self {
// A::One(v)
// }
// }
// ```
pub(crate) fn generate_from_impl_for_enum(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
let variant = ctx.find_node_at_offset::<ast::Variant>()?;
let variant_name = variant.name()?;
let enum_name = variant.parent_enum().name()?;
let field_list = match variant.kind() {
ast::StructKind::Tuple(field_list) => field_list,
_ => return None,
};
if field_list.fields().count() != 1 {
return None;
}
let field_type = field_list.fields().next()?.ty()?;
let path = match field_type {
ast::Type::PathType(it) => it,
_ => return None,
};
if existing_from_impl(&ctx.sema, &variant).is_some() {
mark::hit!(test_add_from_impl_already_exists);
return None;
}
let target = variant.syntax().text_range();
acc.add(
AssistId("generate_from_impl_for_enum", AssistKind::Generate),
"Generate `From` impl for this enum variant",
target,
|edit| {
let start_offset = variant.parent_enum().syntax().text_range().end();
let buf = format!(
r#"
impl From<{0}> for {1} {{
fn from(v: {0}) -> Self {{
{1}::{2}(v)
}}
}}"#,
path.syntax(),
enum_name,
variant_name
);
edit.insert(start_offset, buf);
},
)
}
fn existing_from_impl(
sema: &'_ hir::Semantics<'_, RootDatabase>,
variant: &ast::Variant,
) -> Option<()> {
let variant = sema.to_def(variant)?;
let enum_ = variant.parent_enum(sema.db);
let krate = enum_.module(sema.db).krate();
let from_trait = FamousDefs(sema, krate).core_convert_From()?;
let enum_type = enum_.ty(sema.db);
let wrapped_type = variant.fields(sema.db).get(0)?.signature_ty(sema.db);
if enum_type.impls_trait(sema.db, from_trait, &[wrapped_type]) {
Some(())
} else {
None
}
}
#[cfg(test)]
mod tests {
use test_utils::mark;
use crate::tests::{check_assist, check_assist_not_applicable};
use super::*;
#[test]
fn test_generate_from_impl_for_enum() {
check_assist(
generate_from_impl_for_enum,
"enum A { <|>One(u32) }",
r#"enum A { One(u32) }
impl From<u32> for A {
fn from(v: u32) -> Self {
A::One(v)
}
}"#,
);
}
#[test]
fn test_generate_from_impl_for_enum_complicated_path() {
check_assist(
generate_from_impl_for_enum,
r#"enum A { <|>One(foo::bar::baz::Boo) }"#,
r#"enum A { One(foo::bar::baz::Boo) }
impl From<foo::bar::baz::Boo> for A {
fn from(v: foo::bar::baz::Boo) -> Self {
A::One(v)
}
}"#,
);
}
fn check_not_applicable(ra_fixture: &str) {
let fixture =
format!("//- /main.rs crate:main deps:core\n{}\n{}", ra_fixture, FamousDefs::FIXTURE);
check_assist_not_applicable(generate_from_impl_for_enum, &fixture)
}
#[test]
fn test_add_from_impl_no_element() {
check_not_applicable("enum A { <|>One }");
}
#[test]
fn test_add_from_impl_more_than_one_element_in_tuple() {
check_not_applicable("enum A { <|>One(u32, String) }");
}
#[test]
fn test_add_from_impl_struct_variant() {
check_not_applicable("enum A { <|>One { x: u32 } }");
}
#[test]
fn test_add_from_impl_already_exists() {
mark::check!(test_add_from_impl_already_exists);
check_not_applicable(
r#"
enum A { <|>One(u32), }
impl From<u32> for A {
fn from(v: u32) -> Self {
A::One(v)
}
}
"#,
);
}
#[test]
fn test_add_from_impl_different_variant_impl_exists() {
check_assist(
generate_from_impl_for_enum,
r#"enum A { <|>One(u32), Two(String), }
impl From<String> for A {
fn from(v: String) -> Self {
A::Two(v)
}
}
pub trait From<T> {
fn from(T) -> Self;
}"#,
r#"enum A { One(u32), Two(String), }
impl From<u32> for A {
fn from(v: u32) -> Self {
A::One(v)
}
}
impl From<String> for A {
fn from(v: String) -> Self {
A::Two(v)
}
}
pub trait From<T> {
fn from(T) -> Self;
}"#,
);
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,110 @@
use itertools::Itertools;
use stdx::format_to;
use syntax::ast::{self, AstNode, GenericParamsOwner, NameOwner};
use crate::{AssistContext, AssistId, AssistKind, Assists};
// Assist: generate_impl
//
// Adds a new inherent impl for a type.
//
// ```
// struct Ctx<T: Clone> {
// data: T,<|>
// }
// ```
// ->
// ```
// struct Ctx<T: Clone> {
// data: T,
// }
//
// impl<T: Clone> Ctx<T> {
// $0
// }
// ```
pub(crate) fn generate_impl(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
let nominal = ctx.find_node_at_offset::<ast::AdtDef>()?;
let name = nominal.name()?;
let target = nominal.syntax().text_range();
acc.add(
AssistId("generate_impl", AssistKind::Generate),
format!("Generate impl for `{}`", name),
target,
|edit| {
let type_params = nominal.generic_param_list();
let start_offset = nominal.syntax().text_range().end();
let mut buf = String::new();
buf.push_str("\n\nimpl");
if let Some(type_params) = &type_params {
format_to!(buf, "{}", type_params.syntax());
}
buf.push_str(" ");
buf.push_str(name.text().as_str());
if let Some(type_params) = type_params {
let lifetime_params = type_params
.lifetime_params()
.filter_map(|it| it.lifetime_token())
.map(|it| it.text().clone());
let type_params = type_params
.type_params()
.filter_map(|it| it.name())
.map(|it| it.text().clone());
let generic_params = lifetime_params.chain(type_params).format(", ");
format_to!(buf, "<{}>", generic_params)
}
match ctx.config.snippet_cap {
Some(cap) => {
buf.push_str(" {\n $0\n}");
edit.insert_snippet(cap, start_offset, buf);
}
None => {
buf.push_str(" {\n}");
edit.insert(start_offset, buf);
}
}
},
)
}
#[cfg(test)]
mod tests {
use crate::tests::{check_assist, check_assist_target};
use super::*;
#[test]
fn test_add_impl() {
check_assist(
generate_impl,
"struct Foo {<|>}\n",
"struct Foo {}\n\nimpl Foo {\n $0\n}\n",
);
check_assist(
generate_impl,
"struct Foo<T: Clone> {<|>}",
"struct Foo<T: Clone> {}\n\nimpl<T: Clone> Foo<T> {\n $0\n}",
);
check_assist(
generate_impl,
"struct Foo<'a, T: Foo<'a>> {<|>}",
"struct Foo<'a, T: Foo<'a>> {}\n\nimpl<'a, T: Foo<'a>> Foo<'a, T> {\n $0\n}",
);
}
#[test]
fn add_impl_target() {
check_assist_target(
generate_impl,
"
struct SomeThingIrrelevant;
/// Has a lifetime parameter
struct Foo<'a, T: Foo<'a>> {<|>}
struct EvenMoreIrrelevant;
",
"/// Has a lifetime parameter
struct Foo<'a, T: Foo<'a>> {}",
);
}
}

View file

@ -0,0 +1,421 @@
use hir::Adt;
use itertools::Itertools;
use stdx::format_to;
use syntax::{
ast::{self, AstNode, GenericParamsOwner, NameOwner, StructKind, VisibilityOwner},
T,
};
use crate::{AssistContext, AssistId, AssistKind, Assists};
// Assist: generate_new
//
// Adds a new inherent impl for a type.
//
// ```
// struct Ctx<T: Clone> {
// data: T,<|>
// }
// ```
// ->
// ```
// struct Ctx<T: Clone> {
// data: T,
// }
//
// impl<T: Clone> Ctx<T> {
// fn $0new(data: T) -> Self { Self { data } }
// }
//
// ```
pub(crate) fn generate_new(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
let strukt = ctx.find_node_at_offset::<ast::Struct>()?;
// We want to only apply this to non-union structs with named fields
let field_list = match strukt.kind() {
StructKind::Record(named) => named,
_ => return None,
};
// Return early if we've found an existing new fn
let impl_def = find_struct_impl(&ctx, &strukt)?;
let target = strukt.syntax().text_range();
acc.add(AssistId("generate_new", AssistKind::Generate), "Generate `new`", target, |builder| {
let mut buf = String::with_capacity(512);
if impl_def.is_some() {
buf.push('\n');
}
let vis = strukt.visibility().map_or(String::new(), |v| format!("{} ", v));
let params = field_list
.fields()
.filter_map(|f| Some(format!("{}: {}", f.name()?.syntax(), f.ty()?.syntax())))
.format(", ");
let fields = field_list.fields().filter_map(|f| f.name()).format(", ");
format_to!(buf, " {}fn new({}) -> Self {{ Self {{ {} }} }}", vis, params, fields);
let start_offset = impl_def
.and_then(|impl_def| {
buf.push('\n');
let start = impl_def
.syntax()
.descendants_with_tokens()
.find(|t| t.kind() == T!['{'])?
.text_range()
.end();
Some(start)
})
.unwrap_or_else(|| {
buf = generate_impl_text(&strukt, &buf);
strukt.syntax().text_range().end()
});
match ctx.config.snippet_cap {
None => builder.insert(start_offset, buf),
Some(cap) => {
buf = buf.replace("fn new", "fn $0new");
builder.insert_snippet(cap, start_offset, buf);
}
}
})
}
// Generates the surrounding `impl Type { <code> }` including type and lifetime
// parameters
fn generate_impl_text(strukt: &ast::Struct, code: &str) -> String {
let type_params = strukt.generic_param_list();
let mut buf = String::with_capacity(code.len());
buf.push_str("\n\nimpl");
if let Some(type_params) = &type_params {
format_to!(buf, "{}", type_params.syntax());
}
buf.push_str(" ");
buf.push_str(strukt.name().unwrap().text().as_str());
if let Some(type_params) = type_params {
let lifetime_params = type_params
.lifetime_params()
.filter_map(|it| it.lifetime_token())
.map(|it| it.text().clone());
let type_params =
type_params.type_params().filter_map(|it| it.name()).map(|it| it.text().clone());
format_to!(buf, "<{}>", lifetime_params.chain(type_params).format(", "))
}
format_to!(buf, " {{\n{}\n}}\n", code);
buf
}
// Uses a syntax-driven approach to find any impl blocks for the struct that
// exist within the module/file
//
// Returns `None` if we've found an existing `new` fn
//
// FIXME: change the new fn checking to a more semantic approach when that's more
// viable (e.g. we process proc macros, etc)
fn find_struct_impl(ctx: &AssistContext, strukt: &ast::Struct) -> Option<Option<ast::Impl>> {
let db = ctx.db();
let module = strukt.syntax().ancestors().find(|node| {
ast::Module::can_cast(node.kind()) || ast::SourceFile::can_cast(node.kind())
})?;
let struct_def = ctx.sema.to_def(strukt)?;
let block = module.descendants().filter_map(ast::Impl::cast).find_map(|impl_blk| {
let blk = ctx.sema.to_def(&impl_blk)?;
// FIXME: handle e.g. `struct S<T>; impl<U> S<U> {}`
// (we currently use the wrong type parameter)
// also we wouldn't want to use e.g. `impl S<u32>`
let same_ty = match blk.target_ty(db).as_adt() {
Some(def) => def == Adt::Struct(struct_def),
None => false,
};
let not_trait_impl = blk.target_trait(db).is_none();
if !(same_ty && not_trait_impl) {
None
} else {
Some(impl_blk)
}
});
if let Some(ref impl_blk) = block {
if has_new_fn(impl_blk) {
return None;
}
}
Some(block)
}
fn has_new_fn(imp: &ast::Impl) -> bool {
if let Some(il) = imp.assoc_item_list() {
for item in il.assoc_items() {
if let ast::AssocItem::Fn(f) = item {
if let Some(name) = f.name() {
if name.text().eq_ignore_ascii_case("new") {
return true;
}
}
}
}
}
false
}
#[cfg(test)]
mod tests {
use crate::tests::{check_assist, check_assist_not_applicable, check_assist_target};
use super::*;
#[test]
#[rustfmt::skip]
fn test_generate_new() {
// Check output of generation
check_assist(
generate_new,
"struct Foo {<|>}",
"struct Foo {}
impl Foo {
fn $0new() -> Self { Self { } }
}
",
);
check_assist(
generate_new,
"struct Foo<T: Clone> {<|>}",
"struct Foo<T: Clone> {}
impl<T: Clone> Foo<T> {
fn $0new() -> Self { Self { } }
}
",
);
check_assist(
generate_new,
"struct Foo<'a, T: Foo<'a>> {<|>}",
"struct Foo<'a, T: Foo<'a>> {}
impl<'a, T: Foo<'a>> Foo<'a, T> {
fn $0new() -> Self { Self { } }
}
",
);
check_assist(
generate_new,
"struct Foo { baz: String <|>}",
"struct Foo { baz: String }
impl Foo {
fn $0new(baz: String) -> Self { Self { baz } }
}
",
);
check_assist(
generate_new,
"struct Foo { baz: String, qux: Vec<i32> <|>}",
"struct Foo { baz: String, qux: Vec<i32> }
impl Foo {
fn $0new(baz: String, qux: Vec<i32>) -> Self { Self { baz, qux } }
}
",
);
// Check that visibility modifiers don't get brought in for fields
check_assist(
generate_new,
"struct Foo { pub baz: String, pub qux: Vec<i32> <|>}",
"struct Foo { pub baz: String, pub qux: Vec<i32> }
impl Foo {
fn $0new(baz: String, qux: Vec<i32>) -> Self { Self { baz, qux } }
}
",
);
// Check that it reuses existing impls
check_assist(
generate_new,
"struct Foo {<|>}
impl Foo {}
",
"struct Foo {}
impl Foo {
fn $0new() -> Self { Self { } }
}
",
);
check_assist(
generate_new,
"struct Foo {<|>}
impl Foo {
fn qux(&self) {}
}
",
"struct Foo {}
impl Foo {
fn $0new() -> Self { Self { } }
fn qux(&self) {}
}
",
);
check_assist(
generate_new,
"struct Foo {<|>}
impl Foo {
fn qux(&self) {}
fn baz() -> i32 {
5
}
}
",
"struct Foo {}
impl Foo {
fn $0new() -> Self { Self { } }
fn qux(&self) {}
fn baz() -> i32 {
5
}
}
",
);
// Check visibility of new fn based on struct
check_assist(
generate_new,
"pub struct Foo {<|>}",
"pub struct Foo {}
impl Foo {
pub fn $0new() -> Self { Self { } }
}
",
);
check_assist(
generate_new,
"pub(crate) struct Foo {<|>}",
"pub(crate) struct Foo {}
impl Foo {
pub(crate) fn $0new() -> Self { Self { } }
}
",
);
}
#[test]
fn generate_new_not_applicable_if_fn_exists() {
check_assist_not_applicable(
generate_new,
"
struct Foo {<|>}
impl Foo {
fn new() -> Self {
Self
}
}",
);
check_assist_not_applicable(
generate_new,
"
struct Foo {<|>}
impl Foo {
fn New() -> Self {
Self
}
}",
);
}
#[test]
fn generate_new_target() {
check_assist_target(
generate_new,
"
struct SomeThingIrrelevant;
/// Has a lifetime parameter
struct Foo<'a, T: Foo<'a>> {<|>}
struct EvenMoreIrrelevant;
",
"/// Has a lifetime parameter
struct Foo<'a, T: Foo<'a>> {}",
);
}
#[test]
fn test_unrelated_new() {
check_assist(
generate_new,
r##"
pub struct AstId<N: AstNode> {
file_id: HirFileId,
file_ast_id: FileAstId<N>,
}
impl<N: AstNode> AstId<N> {
pub fn new(file_id: HirFileId, file_ast_id: FileAstId<N>) -> AstId<N> {
AstId { file_id, file_ast_id }
}
}
pub struct Source<T> {
pub file_id: HirFileId,<|>
pub ast: T,
}
impl<T> Source<T> {
pub fn map<F: FnOnce(T) -> U, U>(self, f: F) -> Source<U> {
Source { file_id: self.file_id, ast: f(self.ast) }
}
}
"##,
r##"
pub struct AstId<N: AstNode> {
file_id: HirFileId,
file_ast_id: FileAstId<N>,
}
impl<N: AstNode> AstId<N> {
pub fn new(file_id: HirFileId, file_ast_id: FileAstId<N>) -> AstId<N> {
AstId { file_id, file_ast_id }
}
}
pub struct Source<T> {
pub file_id: HirFileId,
pub ast: T,
}
impl<T> Source<T> {
pub fn $0new(file_id: HirFileId, ast: T) -> Self { Self { file_id, ast } }
pub fn map<F: FnOnce(T) -> U, U>(self, f: F) -> Source<U> {
Source { file_id: self.file_id, ast: f(self.ast) }
}
}
"##,
);
}
}

View file

@ -0,0 +1,695 @@
use ide_db::defs::Definition;
use syntax::{
ast::{self, AstNode, AstToken},
TextRange,
};
use test_utils::mark;
use crate::{
assist_context::{AssistContext, Assists},
AssistId, AssistKind,
};
// Assist: inline_local_variable
//
// Inlines local variable.
//
// ```
// fn main() {
// let x<|> = 1 + 2;
// x * 4;
// }
// ```
// ->
// ```
// fn main() {
// (1 + 2) * 4;
// }
// ```
pub(crate) fn inline_local_variable(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
let let_stmt = ctx.find_node_at_offset::<ast::LetStmt>()?;
let bind_pat = match let_stmt.pat()? {
ast::Pat::IdentPat(pat) => pat,
_ => return None,
};
if bind_pat.mut_token().is_some() {
mark::hit!(test_not_inline_mut_variable);
return None;
}
if !bind_pat.syntax().text_range().contains_inclusive(ctx.offset()) {
mark::hit!(not_applicable_outside_of_bind_pat);
return None;
}
let initializer_expr = let_stmt.initializer()?;
let def = ctx.sema.to_def(&bind_pat)?;
let def = Definition::Local(def);
let refs = def.usages(&ctx.sema).all();
if refs.is_empty() {
mark::hit!(test_not_applicable_if_variable_unused);
return None;
};
let delete_range = if let Some(whitespace) = let_stmt
.syntax()
.next_sibling_or_token()
.and_then(|it| ast::Whitespace::cast(it.as_token()?.clone()))
{
TextRange::new(
let_stmt.syntax().text_range().start(),
whitespace.syntax().text_range().end(),
)
} else {
let_stmt.syntax().text_range()
};
let mut wrap_in_parens = vec![true; refs.len()];
for (i, desc) in refs.iter().enumerate() {
let usage_node = ctx
.covering_node_for_range(desc.file_range.range)
.ancestors()
.find_map(ast::PathExpr::cast)?;
let usage_parent_option = usage_node.syntax().parent().and_then(ast::Expr::cast);
let usage_parent = match usage_parent_option {
Some(u) => u,
None => {
wrap_in_parens[i] = false;
continue;
}
};
wrap_in_parens[i] = match (&initializer_expr, usage_parent) {
(ast::Expr::CallExpr(_), _)
| (ast::Expr::IndexExpr(_), _)
| (ast::Expr::MethodCallExpr(_), _)
| (ast::Expr::FieldExpr(_), _)
| (ast::Expr::TryExpr(_), _)
| (ast::Expr::RefExpr(_), _)
| (ast::Expr::Literal(_), _)
| (ast::Expr::TupleExpr(_), _)
| (ast::Expr::ArrayExpr(_), _)
| (ast::Expr::ParenExpr(_), _)
| (ast::Expr::PathExpr(_), _)
| (ast::Expr::BlockExpr(_), _)
| (ast::Expr::EffectExpr(_), _)
| (_, ast::Expr::CallExpr(_))
| (_, ast::Expr::TupleExpr(_))
| (_, ast::Expr::ArrayExpr(_))
| (_, ast::Expr::ParenExpr(_))
| (_, ast::Expr::ForExpr(_))
| (_, ast::Expr::WhileExpr(_))
| (_, ast::Expr::BreakExpr(_))
| (_, ast::Expr::ReturnExpr(_))
| (_, ast::Expr::MatchExpr(_)) => false,
_ => true,
};
}
let init_str = initializer_expr.syntax().text().to_string();
let init_in_paren = format!("({})", &init_str);
let target = bind_pat.syntax().text_range();
acc.add(
AssistId("inline_local_variable", AssistKind::RefactorInline),
"Inline variable",
target,
move |builder| {
builder.delete(delete_range);
for (desc, should_wrap) in refs.iter().zip(wrap_in_parens) {
let replacement =
if should_wrap { init_in_paren.clone() } else { init_str.clone() };
builder.replace(desc.file_range.range, replacement)
}
},
)
}
#[cfg(test)]
mod tests {
use test_utils::mark;
use crate::tests::{check_assist, check_assist_not_applicable};
use super::*;
#[test]
fn test_inline_let_bind_literal_expr() {
check_assist(
inline_local_variable,
r"
fn bar(a: usize) {}
fn foo() {
let a<|> = 1;
a + 1;
if a > 10 {
}
while a > 10 {
}
let b = a * 10;
bar(a);
}",
r"
fn bar(a: usize) {}
fn foo() {
1 + 1;
if 1 > 10 {
}
while 1 > 10 {
}
let b = 1 * 10;
bar(1);
}",
);
}
#[test]
fn test_inline_let_bind_bin_expr() {
check_assist(
inline_local_variable,
r"
fn bar(a: usize) {}
fn foo() {
let a<|> = 1 + 1;
a + 1;
if a > 10 {
}
while a > 10 {
}
let b = a * 10;
bar(a);
}",
r"
fn bar(a: usize) {}
fn foo() {
(1 + 1) + 1;
if (1 + 1) > 10 {
}
while (1 + 1) > 10 {
}
let b = (1 + 1) * 10;
bar(1 + 1);
}",
);
}
#[test]
fn test_inline_let_bind_function_call_expr() {
check_assist(
inline_local_variable,
r"
fn bar(a: usize) {}
fn foo() {
let a<|> = bar(1);
a + 1;
if a > 10 {
}
while a > 10 {
}
let b = a * 10;
bar(a);
}",
r"
fn bar(a: usize) {}
fn foo() {
bar(1) + 1;
if bar(1) > 10 {
}
while bar(1) > 10 {
}
let b = bar(1) * 10;
bar(bar(1));
}",
);
}
#[test]
fn test_inline_let_bind_cast_expr() {
check_assist(
inline_local_variable,
r"
fn bar(a: usize): usize { a }
fn foo() {
let a<|> = bar(1) as u64;
a + 1;
if a > 10 {
}
while a > 10 {
}
let b = a * 10;
bar(a);
}",
r"
fn bar(a: usize): usize { a }
fn foo() {
(bar(1) as u64) + 1;
if (bar(1) as u64) > 10 {
}
while (bar(1) as u64) > 10 {
}
let b = (bar(1) as u64) * 10;
bar(bar(1) as u64);
}",
);
}
#[test]
fn test_inline_let_bind_block_expr() {
check_assist(
inline_local_variable,
r"
fn foo() {
let a<|> = { 10 + 1 };
a + 1;
if a > 10 {
}
while a > 10 {
}
let b = a * 10;
bar(a);
}",
r"
fn foo() {
{ 10 + 1 } + 1;
if { 10 + 1 } > 10 {
}
while { 10 + 1 } > 10 {
}
let b = { 10 + 1 } * 10;
bar({ 10 + 1 });
}",
);
}
#[test]
fn test_inline_let_bind_paren_expr() {
check_assist(
inline_local_variable,
r"
fn foo() {
let a<|> = ( 10 + 1 );
a + 1;
if a > 10 {
}
while a > 10 {
}
let b = a * 10;
bar(a);
}",
r"
fn foo() {
( 10 + 1 ) + 1;
if ( 10 + 1 ) > 10 {
}
while ( 10 + 1 ) > 10 {
}
let b = ( 10 + 1 ) * 10;
bar(( 10 + 1 ));
}",
);
}
#[test]
fn test_not_inline_mut_variable() {
mark::check!(test_not_inline_mut_variable);
check_assist_not_applicable(
inline_local_variable,
r"
fn foo() {
let mut a<|> = 1 + 1;
a + 1;
}",
);
}
#[test]
fn test_call_expr() {
check_assist(
inline_local_variable,
r"
fn foo() {
let a<|> = bar(10 + 1);
let b = a * 10;
let c = a as usize;
}",
r"
fn foo() {
let b = bar(10 + 1) * 10;
let c = bar(10 + 1) as usize;
}",
);
}
#[test]
fn test_index_expr() {
check_assist(
inline_local_variable,
r"
fn foo() {
let x = vec![1, 2, 3];
let a<|> = x[0];
let b = a * 10;
let c = a as usize;
}",
r"
fn foo() {
let x = vec![1, 2, 3];
let b = x[0] * 10;
let c = x[0] as usize;
}",
);
}
#[test]
fn test_method_call_expr() {
check_assist(
inline_local_variable,
r"
fn foo() {
let bar = vec![1];
let a<|> = bar.len();
let b = a * 10;
let c = a as usize;
}",
r"
fn foo() {
let bar = vec![1];
let b = bar.len() * 10;
let c = bar.len() as usize;
}",
);
}
#[test]
fn test_field_expr() {
check_assist(
inline_local_variable,
r"
struct Bar {
foo: usize
}
fn foo() {
let bar = Bar { foo: 1 };
let a<|> = bar.foo;
let b = a * 10;
let c = a as usize;
}",
r"
struct Bar {
foo: usize
}
fn foo() {
let bar = Bar { foo: 1 };
let b = bar.foo * 10;
let c = bar.foo as usize;
}",
);
}
#[test]
fn test_try_expr() {
check_assist(
inline_local_variable,
r"
fn foo() -> Option<usize> {
let bar = Some(1);
let a<|> = bar?;
let b = a * 10;
let c = a as usize;
None
}",
r"
fn foo() -> Option<usize> {
let bar = Some(1);
let b = bar? * 10;
let c = bar? as usize;
None
}",
);
}
#[test]
fn test_ref_expr() {
check_assist(
inline_local_variable,
r"
fn foo() {
let bar = 10;
let a<|> = &bar;
let b = a * 10;
}",
r"
fn foo() {
let bar = 10;
let b = &bar * 10;
}",
);
}
#[test]
fn test_tuple_expr() {
check_assist(
inline_local_variable,
r"
fn foo() {
let a<|> = (10, 20);
let b = a[0];
}",
r"
fn foo() {
let b = (10, 20)[0];
}",
);
}
#[test]
fn test_array_expr() {
check_assist(
inline_local_variable,
r"
fn foo() {
let a<|> = [1, 2, 3];
let b = a.len();
}",
r"
fn foo() {
let b = [1, 2, 3].len();
}",
);
}
#[test]
fn test_paren() {
check_assist(
inline_local_variable,
r"
fn foo() {
let a<|> = (10 + 20);
let b = a * 10;
let c = a as usize;
}",
r"
fn foo() {
let b = (10 + 20) * 10;
let c = (10 + 20) as usize;
}",
);
}
#[test]
fn test_path_expr() {
check_assist(
inline_local_variable,
r"
fn foo() {
let d = 10;
let a<|> = d;
let b = a * 10;
let c = a as usize;
}",
r"
fn foo() {
let d = 10;
let b = d * 10;
let c = d as usize;
}",
);
}
#[test]
fn test_block_expr() {
check_assist(
inline_local_variable,
r"
fn foo() {
let a<|> = { 10 };
let b = a * 10;
let c = a as usize;
}",
r"
fn foo() {
let b = { 10 } * 10;
let c = { 10 } as usize;
}",
);
}
#[test]
fn test_used_in_different_expr1() {
check_assist(
inline_local_variable,
r"
fn foo() {
let a<|> = 10 + 20;
let b = a * 10;
let c = (a, 20);
let d = [a, 10];
let e = (a);
}",
r"
fn foo() {
let b = (10 + 20) * 10;
let c = (10 + 20, 20);
let d = [10 + 20, 10];
let e = (10 + 20);
}",
);
}
#[test]
fn test_used_in_for_expr() {
check_assist(
inline_local_variable,
r"
fn foo() {
let a<|> = vec![10, 20];
for i in a {}
}",
r"
fn foo() {
for i in vec![10, 20] {}
}",
);
}
#[test]
fn test_used_in_while_expr() {
check_assist(
inline_local_variable,
r"
fn foo() {
let a<|> = 1 > 0;
while a {}
}",
r"
fn foo() {
while 1 > 0 {}
}",
);
}
#[test]
fn test_used_in_break_expr() {
check_assist(
inline_local_variable,
r"
fn foo() {
let a<|> = 1 + 1;
loop {
break a;
}
}",
r"
fn foo() {
loop {
break 1 + 1;
}
}",
);
}
#[test]
fn test_used_in_return_expr() {
check_assist(
inline_local_variable,
r"
fn foo() {
let a<|> = 1 > 0;
return a;
}",
r"
fn foo() {
return 1 > 0;
}",
);
}
#[test]
fn test_used_in_match_expr() {
check_assist(
inline_local_variable,
r"
fn foo() {
let a<|> = 1 > 0;
match a {}
}",
r"
fn foo() {
match 1 > 0 {}
}",
);
}
#[test]
fn test_not_applicable_if_variable_unused() {
mark::check!(test_not_applicable_if_variable_unused);
check_assist_not_applicable(
inline_local_variable,
r"
fn foo() {
let <|>a = 0;
}
",
)
}
#[test]
fn not_applicable_outside_of_bind_pat() {
mark::check!(not_applicable_outside_of_bind_pat);
check_assist_not_applicable(
inline_local_variable,
r"
fn main() {
let x = <|>1 + 2;
x * 4;
}
",
)
}
}

View file

@ -0,0 +1,318 @@
use rustc_hash::FxHashSet;
use syntax::{
ast::{self, GenericParamsOwner, NameOwner},
AstNode, SyntaxKind, TextRange, TextSize,
};
use crate::{assist_context::AssistBuilder, AssistContext, AssistId, AssistKind, Assists};
static ASSIST_NAME: &str = "introduce_named_lifetime";
static ASSIST_LABEL: &str = "Introduce named lifetime";
// Assist: introduce_named_lifetime
//
// Change an anonymous lifetime to a named lifetime.
//
// ```
// impl Cursor<'_<|>> {
// fn node(self) -> &SyntaxNode {
// match self {
// Cursor::Replace(node) | Cursor::Before(node) => node,
// }
// }
// }
// ```
// ->
// ```
// impl<'a> Cursor<'a> {
// fn node(self) -> &SyntaxNode {
// match self {
// Cursor::Replace(node) | Cursor::Before(node) => node,
// }
// }
// }
// ```
// FIXME: How can we handle renaming any one of multiple anonymous lifetimes?
// FIXME: should also add support for the case fun(f: &Foo) -> &<|>Foo
pub(crate) fn introduce_named_lifetime(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
let lifetime_token = ctx
.find_token_at_offset(SyntaxKind::LIFETIME)
.filter(|lifetime| lifetime.text() == "'_")?;
if let Some(fn_def) = lifetime_token.ancestors().find_map(ast::Fn::cast) {
generate_fn_def_assist(acc, &fn_def, lifetime_token.text_range())
} else if let Some(impl_def) = lifetime_token.ancestors().find_map(ast::Impl::cast) {
generate_impl_def_assist(acc, &impl_def, lifetime_token.text_range())
} else {
None
}
}
/// Generate the assist for the fn def case
fn generate_fn_def_assist(
acc: &mut Assists,
fn_def: &ast::Fn,
lifetime_loc: TextRange,
) -> Option<()> {
let param_list: ast::ParamList = fn_def.param_list()?;
let new_lifetime_param = generate_unique_lifetime_param_name(&fn_def.generic_param_list())?;
let end_of_fn_ident = fn_def.name()?.ident_token()?.text_range().end();
let self_param =
// use the self if it's a reference and has no explicit lifetime
param_list.self_param().filter(|p| p.lifetime_token().is_none() && p.amp_token().is_some());
// compute the location which implicitly has the same lifetime as the anonymous lifetime
let loc_needing_lifetime = if let Some(self_param) = self_param {
// if we have a self reference, use that
Some(self_param.self_token()?.text_range().start())
} else {
// otherwise, if there's a single reference parameter without a named liftime, use that
let fn_params_without_lifetime: Vec<_> = param_list
.params()
.filter_map(|param| match param.ty() {
Some(ast::Type::RefType(ascribed_type))
if ascribed_type.lifetime_token() == None =>
{
Some(ascribed_type.amp_token()?.text_range().end())
}
_ => None,
})
.collect();
match fn_params_without_lifetime.len() {
1 => Some(fn_params_without_lifetime.into_iter().nth(0)?),
0 => None,
// multiple unnnamed is invalid. assist is not applicable
_ => return None,
}
};
acc.add(AssistId(ASSIST_NAME, AssistKind::Refactor), ASSIST_LABEL, lifetime_loc, |builder| {
add_lifetime_param(fn_def, builder, end_of_fn_ident, new_lifetime_param);
builder.replace(lifetime_loc, format!("'{}", new_lifetime_param));
loc_needing_lifetime.map(|loc| builder.insert(loc, format!("'{} ", new_lifetime_param)));
})
}
/// Generate the assist for the impl def case
fn generate_impl_def_assist(
acc: &mut Assists,
impl_def: &ast::Impl,
lifetime_loc: TextRange,
) -> Option<()> {
let new_lifetime_param = generate_unique_lifetime_param_name(&impl_def.generic_param_list())?;
let end_of_impl_kw = impl_def.impl_token()?.text_range().end();
acc.add(AssistId(ASSIST_NAME, AssistKind::Refactor), ASSIST_LABEL, lifetime_loc, |builder| {
add_lifetime_param(impl_def, builder, end_of_impl_kw, new_lifetime_param);
builder.replace(lifetime_loc, format!("'{}", new_lifetime_param));
})
}
/// Given a type parameter list, generate a unique lifetime parameter name
/// which is not in the list
fn generate_unique_lifetime_param_name(
existing_type_param_list: &Option<ast::GenericParamList>,
) -> Option<char> {
match existing_type_param_list {
Some(type_params) => {
let used_lifetime_params: FxHashSet<_> = type_params
.lifetime_params()
.map(|p| p.syntax().text().to_string()[1..].to_owned())
.collect();
(b'a'..=b'z').map(char::from).find(|c| !used_lifetime_params.contains(&c.to_string()))
}
None => Some('a'),
}
}
/// Add the lifetime param to `builder`. If there are type parameters in `type_params_owner`, add it to the end. Otherwise
/// add new type params brackets with the lifetime parameter at `new_type_params_loc`.
fn add_lifetime_param<TypeParamsOwner: ast::GenericParamsOwner>(
type_params_owner: &TypeParamsOwner,
builder: &mut AssistBuilder,
new_type_params_loc: TextSize,
new_lifetime_param: char,
) {
match type_params_owner.generic_param_list() {
// add the new lifetime parameter to an existing type param list
Some(type_params) => {
builder.insert(
(u32::from(type_params.syntax().text_range().end()) - 1).into(),
format!(", '{}", new_lifetime_param),
);
}
// create a new type param list containing only the new lifetime parameter
None => {
builder.insert(new_type_params_loc, format!("<'{}>", new_lifetime_param));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tests::{check_assist, check_assist_not_applicable};
#[test]
fn test_example_case() {
check_assist(
introduce_named_lifetime,
r#"impl Cursor<'_<|>> {
fn node(self) -> &SyntaxNode {
match self {
Cursor::Replace(node) | Cursor::Before(node) => node,
}
}
}"#,
r#"impl<'a> Cursor<'a> {
fn node(self) -> &SyntaxNode {
match self {
Cursor::Replace(node) | Cursor::Before(node) => node,
}
}
}"#,
);
}
#[test]
fn test_example_case_simplified() {
check_assist(
introduce_named_lifetime,
r#"impl Cursor<'_<|>> {"#,
r#"impl<'a> Cursor<'a> {"#,
);
}
#[test]
fn test_example_case_cursor_after_tick() {
check_assist(
introduce_named_lifetime,
r#"impl Cursor<'<|>_> {"#,
r#"impl<'a> Cursor<'a> {"#,
);
}
#[test]
fn test_impl_with_other_type_param() {
check_assist(
introduce_named_lifetime,
"impl<I> fmt::Display for SepByBuilder<'_<|>, I>
where
I: Iterator,
I::Item: fmt::Display,
{",
"impl<I, 'a> fmt::Display for SepByBuilder<'a, I>
where
I: Iterator,
I::Item: fmt::Display,
{",
)
}
#[test]
fn test_example_case_cursor_before_tick() {
check_assist(
introduce_named_lifetime,
r#"impl Cursor<<|>'_> {"#,
r#"impl<'a> Cursor<'a> {"#,
);
}
#[test]
fn test_not_applicable_cursor_position() {
check_assist_not_applicable(introduce_named_lifetime, r#"impl Cursor<'_><|> {"#);
check_assist_not_applicable(introduce_named_lifetime, r#"impl Cursor<|><'_> {"#);
}
#[test]
fn test_not_applicable_lifetime_already_name() {
check_assist_not_applicable(introduce_named_lifetime, r#"impl Cursor<'a<|>> {"#);
check_assist_not_applicable(introduce_named_lifetime, r#"fn my_fun<'a>() -> X<'a<|>>"#);
}
#[test]
fn test_with_type_parameter() {
check_assist(
introduce_named_lifetime,
r#"impl<T> Cursor<T, '_<|>>"#,
r#"impl<T, 'a> Cursor<T, 'a>"#,
);
}
#[test]
fn test_with_existing_lifetime_name_conflict() {
check_assist(
introduce_named_lifetime,
r#"impl<'a, 'b> Cursor<'a, 'b, '_<|>>"#,
r#"impl<'a, 'b, 'c> Cursor<'a, 'b, 'c>"#,
);
}
#[test]
fn test_function_return_value_anon_lifetime_param() {
check_assist(
introduce_named_lifetime,
r#"fn my_fun() -> X<'_<|>>"#,
r#"fn my_fun<'a>() -> X<'a>"#,
);
}
#[test]
fn test_function_return_value_anon_reference_lifetime() {
check_assist(
introduce_named_lifetime,
r#"fn my_fun() -> &'_<|> X"#,
r#"fn my_fun<'a>() -> &'a X"#,
);
}
#[test]
fn test_function_param_anon_lifetime() {
check_assist(
introduce_named_lifetime,
r#"fn my_fun(x: X<'_<|>>)"#,
r#"fn my_fun<'a>(x: X<'a>)"#,
);
}
#[test]
fn test_function_add_lifetime_to_params() {
check_assist(
introduce_named_lifetime,
r#"fn my_fun(f: &Foo) -> X<'_<|>>"#,
r#"fn my_fun<'a>(f: &'a Foo) -> X<'a>"#,
);
}
#[test]
fn test_function_add_lifetime_to_params_in_presence_of_other_lifetime() {
check_assist(
introduce_named_lifetime,
r#"fn my_fun<'other>(f: &Foo, b: &'other Bar) -> X<'_<|>>"#,
r#"fn my_fun<'other, 'a>(f: &'a Foo, b: &'other Bar) -> X<'a>"#,
);
}
#[test]
fn test_function_not_applicable_without_self_and_multiple_unnamed_param_lifetimes() {
// this is not permitted under lifetime elision rules
check_assist_not_applicable(
introduce_named_lifetime,
r#"fn my_fun(f: &Foo, b: &Bar) -> X<'_<|>>"#,
);
}
#[test]
fn test_function_add_lifetime_to_self_ref_param() {
check_assist(
introduce_named_lifetime,
r#"fn my_fun<'other>(&self, f: &Foo, b: &'other Bar) -> X<'_<|>>"#,
r#"fn my_fun<'other, 'a>(&'a self, f: &Foo, b: &'other Bar) -> X<'a>"#,
);
}
#[test]
fn test_function_add_lifetime_to_param_with_non_ref_self() {
check_assist(
introduce_named_lifetime,
r#"fn my_fun<'other>(self, f: &Foo, b: &'other Bar) -> X<'_<|>>"#,
r#"fn my_fun<'other, 'a>(self, f: &'a Foo, b: &'other Bar) -> X<'a>"#,
);
}
}

View file

@ -0,0 +1,109 @@
use syntax::{
ast::{self, AstNode},
T,
};
use crate::{
assist_context::{AssistContext, Assists},
utils::invert_boolean_expression,
AssistId, AssistKind,
};
// Assist: invert_if
//
// Apply invert_if
// This transforms if expressions of the form `if !x {A} else {B}` into `if x {B} else {A}`
// This also works with `!=`. This assist can only be applied with the cursor
// on `if`.
//
// ```
// fn main() {
// if<|> !y { A } else { B }
// }
// ```
// ->
// ```
// fn main() {
// if y { B } else { A }
// }
// ```
pub(crate) fn invert_if(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
let if_keyword = ctx.find_token_at_offset(T![if])?;
let expr = ast::IfExpr::cast(if_keyword.parent())?;
let if_range = if_keyword.text_range();
let cursor_in_range = if_range.contains_range(ctx.frange.range);
if !cursor_in_range {
return None;
}
// This assist should not apply for if-let.
if expr.condition()?.pat().is_some() {
return None;
}
let cond = expr.condition()?.expr()?;
let then_node = expr.then_branch()?.syntax().clone();
let else_block = match expr.else_branch()? {
ast::ElseBranch::Block(it) => it,
ast::ElseBranch::IfExpr(_) => return None,
};
let cond_range = cond.syntax().text_range();
let flip_cond = invert_boolean_expression(cond);
let else_node = else_block.syntax();
let else_range = else_node.text_range();
let then_range = then_node.text_range();
acc.add(AssistId("invert_if", AssistKind::RefactorRewrite), "Invert if", if_range, |edit| {
edit.replace(cond_range, flip_cond.syntax().text());
edit.replace(else_range, then_node.text());
edit.replace(then_range, else_node.text());
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tests::{check_assist, check_assist_not_applicable};
#[test]
fn invert_if_remove_inequality() {
check_assist(
invert_if,
"fn f() { i<|>f x != 3 { 1 } else { 3 + 2 } }",
"fn f() { if x == 3 { 3 + 2 } else { 1 } }",
)
}
#[test]
fn invert_if_remove_not() {
check_assist(
invert_if,
"fn f() { <|>if !cond { 3 * 2 } else { 1 } }",
"fn f() { if cond { 1 } else { 3 * 2 } }",
)
}
#[test]
fn invert_if_general_case() {
check_assist(
invert_if,
"fn f() { i<|>f cond { 3 * 2 } else { 1 } }",
"fn f() { if !cond { 1 } else { 3 * 2 } }",
)
}
#[test]
fn invert_if_doesnt_apply_with_cursor_not_on_if() {
check_assist_not_applicable(invert_if, "fn f() { if !<|>cond { 3 * 2 } else { 1 } }")
}
#[test]
fn invert_if_doesnt_apply_with_if_let() {
check_assist_not_applicable(
invert_if,
"fn f() { i<|>f let Some(_) = Some(1) { 1 } else { 0 } }",
)
}
}

View file

@ -0,0 +1,318 @@
use std::iter::successors;
use syntax::{
algo::{neighbor, skip_trivia_token, SyntaxRewriter},
ast::{self, edit::AstNodeEdit, make},
AstNode, Direction, InsertPosition, SyntaxElement, T,
};
use crate::{
assist_context::{AssistContext, Assists},
utils::next_prev,
AssistId, AssistKind,
};
// Assist: merge_imports
//
// Merges two imports with a common prefix.
//
// ```
// use std::<|>fmt::Formatter;
// use std::io;
// ```
// ->
// ```
// use std::{fmt::Formatter, io};
// ```
pub(crate) fn merge_imports(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
let tree: ast::UseTree = ctx.find_node_at_offset()?;
let mut rewriter = SyntaxRewriter::default();
let mut offset = ctx.offset();
if let Some(use_item) = tree.syntax().parent().and_then(ast::Use::cast) {
let (merged, to_delete) = next_prev()
.filter_map(|dir| neighbor(&use_item, dir))
.filter_map(|it| Some((it.clone(), it.use_tree()?)))
.find_map(|(use_item, use_tree)| {
Some((try_merge_trees(&tree, &use_tree)?, use_item))
})?;
rewriter.replace_ast(&tree, &merged);
rewriter += to_delete.remove();
if to_delete.syntax().text_range().end() < offset {
offset -= to_delete.syntax().text_range().len();
}
} else {
let (merged, to_delete) = next_prev()
.filter_map(|dir| neighbor(&tree, dir))
.find_map(|use_tree| Some((try_merge_trees(&tree, &use_tree)?, use_tree.clone())))?;
rewriter.replace_ast(&tree, &merged);
rewriter += to_delete.remove();
if to_delete.syntax().text_range().end() < offset {
offset -= to_delete.syntax().text_range().len();
}
};
let target = tree.syntax().text_range();
acc.add(
AssistId("merge_imports", AssistKind::RefactorRewrite),
"Merge imports",
target,
|builder| {
builder.rewrite(rewriter);
},
)
}
fn try_merge_trees(old: &ast::UseTree, new: &ast::UseTree) -> Option<ast::UseTree> {
let lhs_path = old.path()?;
let rhs_path = new.path()?;
let (lhs_prefix, rhs_prefix) = common_prefix(&lhs_path, &rhs_path)?;
let lhs = old.split_prefix(&lhs_prefix);
let rhs = new.split_prefix(&rhs_prefix);
let should_insert_comma = lhs
.use_tree_list()?
.r_curly_token()
.and_then(|it| skip_trivia_token(it.prev_token()?, Direction::Prev))
.map(|it| it.kind() != T![,])
.unwrap_or(true);
let mut to_insert: Vec<SyntaxElement> = Vec::new();
if should_insert_comma {
to_insert.push(make::token(T![,]).into());
to_insert.push(make::tokens::single_space().into());
}
to_insert.extend(
rhs.use_tree_list()?
.syntax()
.children_with_tokens()
.filter(|it| it.kind() != T!['{'] && it.kind() != T!['}']),
);
let use_tree_list = lhs.use_tree_list()?;
let pos = InsertPosition::Before(use_tree_list.r_curly_token()?.into());
let use_tree_list = use_tree_list.insert_children(pos, to_insert);
Some(lhs.with_use_tree_list(use_tree_list))
}
fn common_prefix(lhs: &ast::Path, rhs: &ast::Path) -> Option<(ast::Path, ast::Path)> {
let mut res = None;
let mut lhs_curr = first_path(&lhs);
let mut rhs_curr = first_path(&rhs);
loop {
match (lhs_curr.segment(), rhs_curr.segment()) {
(Some(lhs), Some(rhs)) if lhs.syntax().text() == rhs.syntax().text() => (),
_ => break,
}
res = Some((lhs_curr.clone(), rhs_curr.clone()));
match (lhs_curr.parent_path(), rhs_curr.parent_path()) {
(Some(lhs), Some(rhs)) => {
lhs_curr = lhs;
rhs_curr = rhs;
}
_ => break,
}
}
res
}
fn first_path(path: &ast::Path) -> ast::Path {
successors(Some(path.clone()), |it| it.qualifier()).last().unwrap()
}
#[cfg(test)]
mod tests {
use crate::tests::{check_assist, check_assist_not_applicable};
use super::*;
#[test]
fn test_merge_first() {
check_assist(
merge_imports,
r"
use std::fmt<|>::Debug;
use std::fmt::Display;
",
r"
use std::fmt::{Debug, Display};
",
)
}
#[test]
fn test_merge_second() {
check_assist(
merge_imports,
r"
use std::fmt::Debug;
use std::fmt<|>::Display;
",
r"
use std::fmt::{Display, Debug};
",
);
}
#[test]
fn merge_self1() {
check_assist(
merge_imports,
r"
use std::fmt<|>;
use std::fmt::Display;
",
r"
use std::fmt::{self, Display};
",
);
}
#[test]
fn merge_self2() {
check_assist(
merge_imports,
r"
use std::{fmt, <|>fmt::Display};
",
r"
use std::{fmt::{Display, self}};
",
);
}
#[test]
fn test_merge_nested() {
check_assist(
merge_imports,
r"
use std::{fmt<|>::Debug, fmt::Display};
",
r"
use std::{fmt::{Debug, Display}};
",
);
check_assist(
merge_imports,
r"
use std::{fmt::Debug, fmt<|>::Display};
",
r"
use std::{fmt::{Display, Debug}};
",
);
}
#[test]
fn test_merge_single_wildcard_diff_prefixes() {
check_assist(
merge_imports,
r"
use std<|>::cell::*;
use std::str;
",
r"
use std::{cell::*, str};
",
)
}
#[test]
fn test_merge_both_wildcard_diff_prefixes() {
check_assist(
merge_imports,
r"
use std<|>::cell::*;
use std::str::*;
",
r"
use std::{cell::*, str::*};
",
)
}
#[test]
fn removes_just_enough_whitespace() {
check_assist(
merge_imports,
r"
use foo<|>::bar;
use foo::baz;
/// Doc comment
",
r"
use foo::{bar, baz};
/// Doc comment
",
);
}
#[test]
fn works_with_trailing_comma() {
check_assist(
merge_imports,
r"
use {
foo<|>::bar,
foo::baz,
};
",
r"
use {
foo::{bar, baz},
};
",
);
check_assist(
merge_imports,
r"
use {
foo::baz,
foo<|>::bar,
};
",
r"
use {
foo::{bar, baz},
};
",
);
}
#[test]
fn test_double_comma() {
check_assist(
merge_imports,
r"
use foo::bar::baz;
use foo::<|>{
FooBar,
};
",
r"
use foo::{
FooBar,
bar::baz};
",
)
}
#[test]
fn test_empty_use() {
check_assist_not_applicable(
merge_imports,
r"
use std::<|>
fn main() {}",
);
}
}

View file

@ -0,0 +1,248 @@
use std::iter::successors;
use syntax::{
algo::neighbor,
ast::{self, AstNode},
Direction,
};
use crate::{AssistContext, AssistId, AssistKind, Assists, TextRange};
// Assist: merge_match_arms
//
// Merges identical match arms.
//
// ```
// enum Action { Move { distance: u32 }, Stop }
//
// fn handle(action: Action) {
// match action {
// <|>Action::Move(..) => foo(),
// Action::Stop => foo(),
// }
// }
// ```
// ->
// ```
// enum Action { Move { distance: u32 }, Stop }
//
// fn handle(action: Action) {
// match action {
// Action::Move(..) | Action::Stop => foo(),
// }
// }
// ```
pub(crate) fn merge_match_arms(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
let current_arm = ctx.find_node_at_offset::<ast::MatchArm>()?;
// Don't try to handle arms with guards for now - can add support for this later
if current_arm.guard().is_some() {
return None;
}
let current_expr = current_arm.expr()?;
let current_text_range = current_arm.syntax().text_range();
// We check if the following match arms match this one. We could, but don't,
// compare to the previous match arm as well.
let arms_to_merge = successors(Some(current_arm), |it| neighbor(it, Direction::Next))
.take_while(|arm| {
if arm.guard().is_some() {
return false;
}
match arm.expr() {
Some(expr) => expr.syntax().text() == current_expr.syntax().text(),
None => false,
}
})
.collect::<Vec<_>>();
if arms_to_merge.len() <= 1 {
return None;
}
acc.add(
AssistId("merge_match_arms", AssistKind::RefactorRewrite),
"Merge match arms",
current_text_range,
|edit| {
let pats = if arms_to_merge.iter().any(contains_placeholder) {
"_".into()
} else {
arms_to_merge
.iter()
.filter_map(ast::MatchArm::pat)
.map(|x| x.syntax().to_string())
.collect::<Vec<String>>()
.join(" | ")
};
let arm = format!("{} => {}", pats, current_expr.syntax().text());
let start = arms_to_merge.first().unwrap().syntax().text_range().start();
let end = arms_to_merge.last().unwrap().syntax().text_range().end();
edit.replace(TextRange::new(start, end), arm);
},
)
}
fn contains_placeholder(a: &ast::MatchArm) -> bool {
matches!(a.pat(), Some(ast::Pat::WildcardPat(..)))
}
#[cfg(test)]
mod tests {
use crate::tests::{check_assist, check_assist_not_applicable};
use super::*;
#[test]
fn merge_match_arms_single_patterns() {
check_assist(
merge_match_arms,
r#"
#[derive(Debug)]
enum X { A, B, C }
fn main() {
let x = X::A;
let y = match x {
X::A => { 1i32<|> }
X::B => { 1i32 }
X::C => { 2i32 }
}
}
"#,
r#"
#[derive(Debug)]
enum X { A, B, C }
fn main() {
let x = X::A;
let y = match x {
X::A | X::B => { 1i32 }
X::C => { 2i32 }
}
}
"#,
);
}
#[test]
fn merge_match_arms_multiple_patterns() {
check_assist(
merge_match_arms,
r#"
#[derive(Debug)]
enum X { A, B, C, D, E }
fn main() {
let x = X::A;
let y = match x {
X::A | X::B => {<|> 1i32 },
X::C | X::D => { 1i32 },
X::E => { 2i32 },
}
}
"#,
r#"
#[derive(Debug)]
enum X { A, B, C, D, E }
fn main() {
let x = X::A;
let y = match x {
X::A | X::B | X::C | X::D => { 1i32 },
X::E => { 2i32 },
}
}
"#,
);
}
#[test]
fn merge_match_arms_placeholder_pattern() {
check_assist(
merge_match_arms,
r#"
#[derive(Debug)]
enum X { A, B, C, D, E }
fn main() {
let x = X::A;
let y = match x {
X::A => { 1i32 },
X::B => { 2i<|>32 },
_ => { 2i32 }
}
}
"#,
r#"
#[derive(Debug)]
enum X { A, B, C, D, E }
fn main() {
let x = X::A;
let y = match x {
X::A => { 1i32 },
_ => { 2i32 }
}
}
"#,
);
}
#[test]
fn merges_all_subsequent_arms() {
check_assist(
merge_match_arms,
r#"
enum X { A, B, C, D, E }
fn main() {
match X::A {
X::A<|> => 92,
X::B => 92,
X::C => 92,
X::D => 62,
_ => panic!(),
}
}
"#,
r#"
enum X { A, B, C, D, E }
fn main() {
match X::A {
X::A | X::B | X::C => 92,
X::D => 62,
_ => panic!(),
}
}
"#,
)
}
#[test]
fn merge_match_arms_rejects_guards() {
check_assist_not_applicable(
merge_match_arms,
r#"
#[derive(Debug)]
enum X {
A(i32),
B,
C
}
fn main() {
let x = X::A;
let y = match x {
X::A(a) if a > 5 => { <|>1i32 },
X::B => { 1i32 },
X::C => { 2i32 }
}
}
"#,
);
}
}

View file

@ -0,0 +1,152 @@
use syntax::{
ast::{self, edit::AstNodeEdit, make, AstNode, NameOwner, TypeBoundsOwner},
match_ast,
SyntaxKind::*,
T,
};
use crate::{AssistContext, AssistId, AssistKind, Assists};
// Assist: move_bounds_to_where_clause
//
// Moves inline type bounds to a where clause.
//
// ```
// fn apply<T, U, <|>F: FnOnce(T) -> U>(f: F, x: T) -> U {
// f(x)
// }
// ```
// ->
// ```
// fn apply<T, U, F>(f: F, x: T) -> U where F: FnOnce(T) -> U {
// f(x)
// }
// ```
pub(crate) fn move_bounds_to_where_clause(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
let type_param_list = ctx.find_node_at_offset::<ast::GenericParamList>()?;
let mut type_params = type_param_list.type_params();
if type_params.all(|p| p.type_bound_list().is_none()) {
return None;
}
let parent = type_param_list.syntax().parent()?;
if parent.children_with_tokens().any(|it| it.kind() == WHERE_CLAUSE) {
return None;
}
let anchor = match_ast! {
match parent {
ast::Fn(it) => it.body()?.syntax().clone().into(),
ast::Trait(it) => it.assoc_item_list()?.syntax().clone().into(),
ast::Impl(it) => it.assoc_item_list()?.syntax().clone().into(),
ast::Enum(it) => it.variant_list()?.syntax().clone().into(),
ast::Struct(it) => {
it.syntax().children_with_tokens()
.find(|it| it.kind() == RECORD_FIELD_LIST || it.kind() == T![;])?
},
_ => return None
}
};
let target = type_param_list.syntax().text_range();
acc.add(
AssistId("move_bounds_to_where_clause", AssistKind::RefactorRewrite),
"Move to where clause",
target,
|edit| {
let new_params = type_param_list
.type_params()
.filter(|it| it.type_bound_list().is_some())
.map(|type_param| {
let without_bounds = type_param.remove_bounds();
(type_param, without_bounds)
});
let new_type_param_list = type_param_list.replace_descendants(new_params);
edit.replace_ast(type_param_list.clone(), new_type_param_list);
let where_clause = {
let predicates = type_param_list.type_params().filter_map(build_predicate);
make::where_clause(predicates)
};
let to_insert = match anchor.prev_sibling_or_token() {
Some(ref elem) if elem.kind() == WHITESPACE => {
format!("{} ", where_clause.syntax())
}
_ => format!(" {}", where_clause.syntax()),
};
edit.insert(anchor.text_range().start(), to_insert);
},
)
}
fn build_predicate(param: ast::TypeParam) -> Option<ast::WherePred> {
let path = {
let name_ref = make::name_ref(&param.name()?.syntax().to_string());
let segment = make::path_segment(name_ref);
make::path_unqualified(segment)
};
let predicate = make::where_pred(path, param.type_bound_list()?.bounds());
Some(predicate)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tests::check_assist;
#[test]
fn move_bounds_to_where_clause_fn() {
check_assist(
move_bounds_to_where_clause,
r#"
fn foo<T: u32, <|>F: FnOnce(T) -> T>() {}
"#,
r#"
fn foo<T, F>() where T: u32, F: FnOnce(T) -> T {}
"#,
);
}
#[test]
fn move_bounds_to_where_clause_impl() {
check_assist(
move_bounds_to_where_clause,
r#"
impl<U: u32, <|>T> A<U, T> {}
"#,
r#"
impl<U, T> A<U, T> where U: u32 {}
"#,
);
}
#[test]
fn move_bounds_to_where_clause_struct() {
check_assist(
move_bounds_to_where_clause,
r#"
struct A<<|>T: Iterator<Item = u32>> {}
"#,
r#"
struct A<T> where T: Iterator<Item = u32> {}
"#,
);
}
#[test]
fn move_bounds_to_where_clause_tuple_struct() {
check_assist(
move_bounds_to_where_clause,
r#"
struct Pair<<|>T: u32>(T, T);
"#,
r#"
struct Pair<T>(T, T) where T: u32;
"#,
);
}
}

View file

@ -0,0 +1,293 @@
use syntax::{
ast::{edit::AstNodeEdit, make, AstNode, IfExpr, MatchArm},
SyntaxKind::WHITESPACE,
};
use crate::{AssistContext, AssistId, AssistKind, Assists};
// Assist: move_guard_to_arm_body
//
// Moves match guard into match arm body.
//
// ```
// enum Action { Move { distance: u32 }, Stop }
//
// fn handle(action: Action) {
// match action {
// Action::Move { distance } <|>if distance > 10 => foo(),
// _ => (),
// }
// }
// ```
// ->
// ```
// enum Action { Move { distance: u32 }, Stop }
//
// fn handle(action: Action) {
// match action {
// Action::Move { distance } => if distance > 10 {
// foo()
// },
// _ => (),
// }
// }
// ```
pub(crate) fn move_guard_to_arm_body(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
let match_arm = ctx.find_node_at_offset::<MatchArm>()?;
let guard = match_arm.guard()?;
let space_before_guard = guard.syntax().prev_sibling_or_token();
let guard_condition = guard.expr()?;
let arm_expr = match_arm.expr()?;
let if_expr = make::expr_if(
make::condition(guard_condition, None),
make::block_expr(None, Some(arm_expr.clone())),
)
.indent(arm_expr.indent_level());
let target = guard.syntax().text_range();
acc.add(
AssistId("move_guard_to_arm_body", AssistKind::RefactorRewrite),
"Move guard to arm body",
target,
|edit| {
match space_before_guard {
Some(element) if element.kind() == WHITESPACE => {
edit.delete(element.text_range());
}
_ => (),
};
edit.delete(guard.syntax().text_range());
edit.replace_ast(arm_expr, if_expr);
},
)
}
// Assist: move_arm_cond_to_match_guard
//
// Moves if expression from match arm body into a guard.
//
// ```
// enum Action { Move { distance: u32 }, Stop }
//
// fn handle(action: Action) {
// match action {
// Action::Move { distance } => <|>if distance > 10 { foo() },
// _ => (),
// }
// }
// ```
// ->
// ```
// enum Action { Move { distance: u32 }, Stop }
//
// fn handle(action: Action) {
// match action {
// Action::Move { distance } if distance > 10 => foo(),
// _ => (),
// }
// }
// ```
pub(crate) fn move_arm_cond_to_match_guard(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
let match_arm: MatchArm = ctx.find_node_at_offset::<MatchArm>()?;
let match_pat = match_arm.pat()?;
let arm_body = match_arm.expr()?;
let if_expr: IfExpr = IfExpr::cast(arm_body.syntax().clone())?;
let cond = if_expr.condition()?;
let then_block = if_expr.then_branch()?;
// Not support if with else branch
if if_expr.else_branch().is_some() {
return None;
}
// Not support moving if let to arm guard
if cond.pat().is_some() {
return None;
}
let buf = format!(" if {}", cond.syntax().text());
let target = if_expr.syntax().text_range();
acc.add(
AssistId("move_arm_cond_to_match_guard", AssistKind::RefactorRewrite),
"Move condition to match guard",
target,
|edit| {
let then_only_expr = then_block.statements().next().is_none();
match &then_block.expr() {
Some(then_expr) if then_only_expr => {
edit.replace(if_expr.syntax().text_range(), then_expr.syntax().text())
}
_ => edit.replace(if_expr.syntax().text_range(), then_block.syntax().text()),
}
edit.insert(match_pat.syntax().text_range().end(), buf);
},
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tests::{check_assist, check_assist_not_applicable, check_assist_target};
#[test]
fn move_guard_to_arm_body_target() {
check_assist_target(
move_guard_to_arm_body,
r#"
fn main() {
match 92 {
x <|>if x > 10 => false,
_ => true
}
}
"#,
r#"if x > 10"#,
);
}
#[test]
fn move_guard_to_arm_body_works() {
check_assist(
move_guard_to_arm_body,
r#"
fn main() {
match 92 {
x <|>if x > 10 => false,
_ => true
}
}
"#,
r#"
fn main() {
match 92 {
x => if x > 10 {
false
},
_ => true
}
}
"#,
);
}
#[test]
fn move_guard_to_arm_body_works_complex_match() {
check_assist(
move_guard_to_arm_body,
r#"
fn main() {
match 92 {
<|>x @ 4 | x @ 5 if x > 5 => true,
_ => false
}
}
"#,
r#"
fn main() {
match 92 {
x @ 4 | x @ 5 => if x > 5 {
true
},
_ => false
}
}
"#,
);
}
#[test]
fn move_arm_cond_to_match_guard_works() {
check_assist(
move_arm_cond_to_match_guard,
r#"
fn main() {
match 92 {
x => if x > 10 { <|>false },
_ => true
}
}
"#,
r#"
fn main() {
match 92 {
x if x > 10 => false,
_ => true
}
}
"#,
);
}
#[test]
fn move_arm_cond_to_match_guard_if_let_not_works() {
check_assist_not_applicable(
move_arm_cond_to_match_guard,
r#"
fn main() {
match 92 {
x => if let 62 = x { <|>false },
_ => true
}
}
"#,
);
}
#[test]
fn move_arm_cond_to_match_guard_if_empty_body_works() {
check_assist(
move_arm_cond_to_match_guard,
r#"
fn main() {
match 92 {
x => if x > 10 { <|> },
_ => true
}
}
"#,
r#"
fn main() {
match 92 {
x if x > 10 => { },
_ => true
}
}
"#,
);
}
#[test]
fn move_arm_cond_to_match_guard_if_multiline_body_works() {
check_assist(
move_arm_cond_to_match_guard,
r#"
fn main() {
match 92 {
x => if x > 10 {
92;<|>
false
},
_ => true
}
}
"#,
r#"
fn main() {
match 92 {
x if x > 10 => {
92;
false
},
_ => true
}
}
"#,
);
}
}

View file

@ -0,0 +1,504 @@
use std::borrow::Cow;
use syntax::{
ast::{self, HasQuotes, HasStringValue},
AstToken,
SyntaxKind::{RAW_STRING, STRING},
TextRange, TextSize,
};
use test_utils::mark;
use crate::{AssistContext, AssistId, AssistKind, Assists};
// Assist: make_raw_string
//
// Adds `r#` to a plain string literal.
//
// ```
// fn main() {
// "Hello,<|> World!";
// }
// ```
// ->
// ```
// fn main() {
// r#"Hello, World!"#;
// }
// ```
pub(crate) fn make_raw_string(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
let token = ctx.find_token_at_offset(STRING).and_then(ast::String::cast)?;
let value = token.value()?;
let target = token.syntax().text_range();
acc.add(
AssistId("make_raw_string", AssistKind::RefactorRewrite),
"Rewrite as raw string",
target,
|edit| {
let hashes = "#".repeat(required_hashes(&value).max(1));
if matches!(value, Cow::Borrowed(_)) {
// Avoid replacing the whole string to better position the cursor.
edit.insert(token.syntax().text_range().start(), format!("r{}", hashes));
edit.insert(token.syntax().text_range().end(), format!("{}", hashes));
} else {
edit.replace(
token.syntax().text_range(),
format!("r{}\"{}\"{}", hashes, value, hashes),
);
}
},
)
}
// Assist: make_usual_string
//
// Turns a raw string into a plain string.
//
// ```
// fn main() {
// r#"Hello,<|> "World!""#;
// }
// ```
// ->
// ```
// fn main() {
// "Hello, \"World!\"";
// }
// ```
pub(crate) fn make_usual_string(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
let token = ctx.find_token_at_offset(RAW_STRING).and_then(ast::RawString::cast)?;
let value = token.value()?;
let target = token.syntax().text_range();
acc.add(
AssistId("make_usual_string", AssistKind::RefactorRewrite),
"Rewrite as regular string",
target,
|edit| {
// parse inside string to escape `"`
let escaped = value.escape_default().to_string();
if let Some(offsets) = token.quote_offsets() {
if token.text()[offsets.contents - token.syntax().text_range().start()] == escaped {
edit.replace(offsets.quotes.0, "\"");
edit.replace(offsets.quotes.1, "\"");
return;
}
}
edit.replace(token.syntax().text_range(), format!("\"{}\"", escaped));
},
)
}
// Assist: add_hash
//
// Adds a hash to a raw string literal.
//
// ```
// fn main() {
// r#"Hello,<|> World!"#;
// }
// ```
// ->
// ```
// fn main() {
// r##"Hello, World!"##;
// }
// ```
pub(crate) fn add_hash(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
let token = ctx.find_token_at_offset(RAW_STRING)?;
let target = token.text_range();
acc.add(AssistId("add_hash", AssistKind::Refactor), "Add #", target, |edit| {
edit.insert(token.text_range().start() + TextSize::of('r'), "#");
edit.insert(token.text_range().end(), "#");
})
}
// Assist: remove_hash
//
// Removes a hash from a raw string literal.
//
// ```
// fn main() {
// r#"Hello,<|> World!"#;
// }
// ```
// ->
// ```
// fn main() {
// r"Hello, World!";
// }
// ```
pub(crate) fn remove_hash(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
let token = ctx.find_token_at_offset(RAW_STRING).and_then(ast::RawString::cast)?;
let text = token.text().as_str();
if !text.starts_with("r#") && text.ends_with('#') {
return None;
}
let existing_hashes = text.chars().skip(1).take_while(|&it| it == '#').count();
let text_range = token.syntax().text_range();
let internal_text = &text[token.text_range_between_quotes()? - text_range.start()];
if existing_hashes == required_hashes(internal_text) {
mark::hit!(cant_remove_required_hash);
return None;
}
acc.add(AssistId("remove_hash", AssistKind::RefactorRewrite), "Remove #", text_range, |edit| {
edit.delete(TextRange::at(text_range.start() + TextSize::of('r'), TextSize::of('#')));
edit.delete(TextRange::new(text_range.end() - TextSize::of('#'), text_range.end()));
})
}
fn required_hashes(s: &str) -> usize {
let mut res = 0usize;
for idx in s.match_indices('"').map(|(i, _)| i) {
let (_, sub) = s.split_at(idx + 1);
let n_hashes = sub.chars().take_while(|c| *c == '#').count();
res = res.max(n_hashes + 1)
}
res
}
#[test]
fn test_required_hashes() {
assert_eq!(0, required_hashes("abc"));
assert_eq!(0, required_hashes("###"));
assert_eq!(1, required_hashes("\""));
assert_eq!(2, required_hashes("\"#abc"));
assert_eq!(0, required_hashes("#abc"));
assert_eq!(3, required_hashes("#ab\"##c"));
assert_eq!(5, required_hashes("#ab\"##\"####c"));
}
#[cfg(test)]
mod tests {
use test_utils::mark;
use crate::tests::{check_assist, check_assist_not_applicable, check_assist_target};
use super::*;
#[test]
fn make_raw_string_target() {
check_assist_target(
make_raw_string,
r#"
fn f() {
let s = <|>"random\nstring";
}
"#,
r#""random\nstring""#,
);
}
#[test]
fn make_raw_string_works() {
check_assist(
make_raw_string,
r#"
fn f() {
let s = <|>"random\nstring";
}
"#,
r##"
fn f() {
let s = r#"random
string"#;
}
"##,
)
}
#[test]
fn make_raw_string_works_inside_macros() {
check_assist(
make_raw_string,
r#"
fn f() {
format!(<|>"x = {}", 92)
}
"#,
r##"
fn f() {
format!(r#"x = {}"#, 92)
}
"##,
)
}
#[test]
fn make_raw_string_hashes_inside_works() {
check_assist(
make_raw_string,
r###"
fn f() {
let s = <|>"#random##\nstring";
}
"###,
r####"
fn f() {
let s = r#"#random##
string"#;
}
"####,
)
}
#[test]
fn make_raw_string_closing_hashes_inside_works() {
check_assist(
make_raw_string,
r###"
fn f() {
let s = <|>"#random\"##\nstring";
}
"###,
r####"
fn f() {
let s = r###"#random"##
string"###;
}
"####,
)
}
#[test]
fn make_raw_string_nothing_to_unescape_works() {
check_assist(
make_raw_string,
r#"
fn f() {
let s = <|>"random string";
}
"#,
r##"
fn f() {
let s = r#"random string"#;
}
"##,
)
}
#[test]
fn make_raw_string_not_works_on_partial_string() {
check_assist_not_applicable(
make_raw_string,
r#"
fn f() {
let s = "foo<|>
}
"#,
)
}
#[test]
fn make_usual_string_not_works_on_partial_string() {
check_assist_not_applicable(
make_usual_string,
r#"
fn main() {
let s = r#"bar<|>
}
"#,
)
}
#[test]
fn add_hash_target() {
check_assist_target(
add_hash,
r#"
fn f() {
let s = <|>r"random string";
}
"#,
r#"r"random string""#,
);
}
#[test]
fn add_hash_works() {
check_assist(
add_hash,
r#"
fn f() {
let s = <|>r"random string";
}
"#,
r##"
fn f() {
let s = r#"random string"#;
}
"##,
)
}
#[test]
fn add_more_hash_works() {
check_assist(
add_hash,
r##"
fn f() {
let s = <|>r#"random"string"#;
}
"##,
r###"
fn f() {
let s = r##"random"string"##;
}
"###,
)
}
#[test]
fn add_hash_not_works() {
check_assist_not_applicable(
add_hash,
r#"
fn f() {
let s = <|>"random string";
}
"#,
);
}
#[test]
fn remove_hash_target() {
check_assist_target(
remove_hash,
r##"
fn f() {
let s = <|>r#"random string"#;
}
"##,
r##"r#"random string"#"##,
);
}
#[test]
fn remove_hash_works() {
check_assist(
remove_hash,
r##"fn f() { let s = <|>r#"random string"#; }"##,
r#"fn f() { let s = r"random string"; }"#,
)
}
#[test]
fn cant_remove_required_hash() {
mark::check!(cant_remove_required_hash);
check_assist_not_applicable(
remove_hash,
r##"
fn f() {
let s = <|>r#"random"str"ing"#;
}
"##,
)
}
#[test]
fn remove_more_hash_works() {
check_assist(
remove_hash,
r###"
fn f() {
let s = <|>r##"random string"##;
}
"###,
r##"
fn f() {
let s = r#"random string"#;
}
"##,
)
}
#[test]
fn remove_hash_doesnt_work() {
check_assist_not_applicable(remove_hash, r#"fn f() { let s = <|>"random string"; }"#);
}
#[test]
fn remove_hash_no_hash_doesnt_work() {
check_assist_not_applicable(remove_hash, r#"fn f() { let s = <|>r"random string"; }"#);
}
#[test]
fn make_usual_string_target() {
check_assist_target(
make_usual_string,
r##"
fn f() {
let s = <|>r#"random string"#;
}
"##,
r##"r#"random string"#"##,
);
}
#[test]
fn make_usual_string_works() {
check_assist(
make_usual_string,
r##"
fn f() {
let s = <|>r#"random string"#;
}
"##,
r#"
fn f() {
let s = "random string";
}
"#,
)
}
#[test]
fn make_usual_string_with_quote_works() {
check_assist(
make_usual_string,
r##"
fn f() {
let s = <|>r#"random"str"ing"#;
}
"##,
r#"
fn f() {
let s = "random\"str\"ing";
}
"#,
)
}
#[test]
fn make_usual_string_more_hash_works() {
check_assist(
make_usual_string,
r###"
fn f() {
let s = <|>r##"random string"##;
}
"###,
r##"
fn f() {
let s = "random string";
}
"##,
)
}
#[test]
fn make_usual_string_not_works() {
check_assist_not_applicable(
make_usual_string,
r#"
fn f() {
let s = <|>"random string";
}
"#,
);
}
}

View file

@ -0,0 +1,206 @@
use syntax::{
ast::{self, AstNode},
TextRange, TextSize, T,
};
use crate::{AssistContext, AssistId, AssistKind, Assists};
// Assist: remove_dbg
//
// Removes `dbg!()` macro call.
//
// ```
// fn main() {
// <|>dbg!(92);
// }
// ```
// ->
// ```
// fn main() {
// 92;
// }
// ```
pub(crate) fn remove_dbg(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
let macro_call = ctx.find_node_at_offset::<ast::MacroCall>()?;
if !is_valid_macrocall(&macro_call, "dbg")? {
return None;
}
let is_leaf = macro_call.syntax().next_sibling().is_none();
let macro_end = if macro_call.semicolon_token().is_some() {
macro_call.syntax().text_range().end() - TextSize::of(';')
} else {
macro_call.syntax().text_range().end()
};
// macro_range determines what will be deleted and replaced with macro_content
let macro_range = TextRange::new(macro_call.syntax().text_range().start(), macro_end);
let paste_instead_of_dbg = {
let text = macro_call.token_tree()?.syntax().text();
// leafiness determines if we should include the parenthesis or not
let slice_index: TextRange = if is_leaf {
// leaf means - we can extract the contents of the dbg! in text
TextRange::new(TextSize::of('('), text.len() - TextSize::of(')'))
} else {
// not leaf - means we should keep the parens
TextRange::up_to(text.len())
};
text.slice(slice_index).to_string()
};
let target = macro_call.syntax().text_range();
acc.add(AssistId("remove_dbg", AssistKind::Refactor), "Remove dbg!()", target, |builder| {
builder.replace(macro_range, paste_instead_of_dbg);
})
}
/// Verifies that the given macro_call actually matches the given name
/// and contains proper ending tokens
fn is_valid_macrocall(macro_call: &ast::MacroCall, macro_name: &str) -> Option<bool> {
let path = macro_call.path()?;
let name_ref = path.segment()?.name_ref()?;
// Make sure it is actually a dbg-macro call, dbg followed by !
let excl = path.syntax().next_sibling_or_token()?;
if name_ref.text() != macro_name || excl.kind() != T![!] {
return None;
}
let node = macro_call.token_tree()?.syntax().clone();
let first_child = node.first_child_or_token()?;
let last_child = node.last_child_or_token()?;
match (first_child.kind(), last_child.kind()) {
(T!['('], T![')']) | (T!['['], T![']']) | (T!['{'], T!['}']) => Some(true),
_ => Some(false),
}
}
#[cfg(test)]
mod tests {
use crate::tests::{check_assist, check_assist_not_applicable, check_assist_target};
use super::*;
#[test]
fn test_remove_dbg() {
check_assist(remove_dbg, "<|>dbg!(1 + 1)", "1 + 1");
check_assist(remove_dbg, "dbg!<|>((1 + 1))", "(1 + 1)");
check_assist(remove_dbg, "dbg!(1 <|>+ 1)", "1 + 1");
check_assist(remove_dbg, "let _ = <|>dbg!(1 + 1)", "let _ = 1 + 1");
check_assist(
remove_dbg,
"
fn foo(n: usize) {
if let Some(_) = dbg!(n.<|>checked_sub(4)) {
// ...
}
}
",
"
fn foo(n: usize) {
if let Some(_) = n.checked_sub(4) {
// ...
}
}
",
);
}
#[test]
fn test_remove_dbg_with_brackets_and_braces() {
check_assist(remove_dbg, "dbg![<|>1 + 1]", "1 + 1");
check_assist(remove_dbg, "dbg!{<|>1 + 1}", "1 + 1");
}
#[test]
fn test_remove_dbg_not_applicable() {
check_assist_not_applicable(remove_dbg, "<|>vec![1, 2, 3]");
check_assist_not_applicable(remove_dbg, "<|>dbg(5, 6, 7)");
check_assist_not_applicable(remove_dbg, "<|>dbg!(5, 6, 7");
}
#[test]
fn test_remove_dbg_target() {
check_assist_target(
remove_dbg,
"
fn foo(n: usize) {
if let Some(_) = dbg!(n.<|>checked_sub(4)) {
// ...
}
}
",
"dbg!(n.checked_sub(4))",
);
}
#[test]
fn test_remove_dbg_keep_semicolon() {
// https://github.com/rust-analyzer/rust-analyzer/issues/5129#issuecomment-651399779
// not quite though
// adding a comment at the end of the line makes
// the ast::MacroCall to include the semicolon at the end
check_assist(
remove_dbg,
r#"let res = <|>dbg!(1 * 20); // needless comment"#,
r#"let res = 1 * 20; // needless comment"#,
);
}
#[test]
fn test_remove_dbg_keep_expression() {
check_assist(
remove_dbg,
r#"let res = <|>dbg!(a + b).foo();"#,
r#"let res = (a + b).foo();"#,
);
}
#[test]
fn test_remove_dbg_from_inside_fn() {
check_assist_target(
remove_dbg,
r#"
fn square(x: u32) -> u32 {
x * x
}
fn main() {
let x = square(dbg<|>!(5 + 10));
println!("{}", x);
}"#,
"dbg!(5 + 10)",
);
check_assist(
remove_dbg,
r#"
fn square(x: u32) -> u32 {
x * x
}
fn main() {
let x = square(dbg<|>!(5 + 10));
println!("{}", x);
}"#,
r#"
fn square(x: u32) -> u32 {
x * x
}
fn main() {
let x = square(5 + 10);
println!("{}", x);
}"#,
);
}
}

View file

@ -0,0 +1,37 @@
use syntax::{SyntaxKind, TextRange, T};
use crate::{AssistContext, AssistId, AssistKind, Assists};
// Assist: remove_mut
//
// Removes the `mut` keyword.
//
// ```
// impl Walrus {
// fn feed(&mut<|> self, amount: u32) {}
// }
// ```
// ->
// ```
// impl Walrus {
// fn feed(&self, amount: u32) {}
// }
// ```
pub(crate) fn remove_mut(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
let mut_token = ctx.find_token_at_offset(T![mut])?;
let delete_from = mut_token.text_range().start();
let delete_to = match mut_token.next_token() {
Some(it) if it.kind() == SyntaxKind::WHITESPACE => it.text_range().end(),
_ => mut_token.text_range().end(),
};
let target = mut_token.text_range();
acc.add(
AssistId("remove_mut", AssistKind::Refactor),
"Remove `mut` keyword",
target,
|builder| {
builder.delete(TextRange::new(delete_from, delete_to));
},
)
}

View file

@ -0,0 +1,131 @@
use ide_db::{defs::Definition, search::Reference};
use syntax::{
algo::find_node_at_range,
ast::{self, ArgListOwner},
AstNode, SyntaxNode, TextRange, T,
};
use test_utils::mark;
use crate::{
assist_context::AssistBuilder, utils::next_prev, AssistContext, AssistId, AssistKind, Assists,
};
// Assist: remove_unused_param
//
// Removes unused function parameter.
//
// ```
// fn frobnicate(x: i32<|>) {}
//
// fn main() {
// frobnicate(92);
// }
// ```
// ->
// ```
// fn frobnicate() {}
//
// fn main() {
// frobnicate();
// }
// ```
pub(crate) fn remove_unused_param(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
let param: ast::Param = ctx.find_node_at_offset()?;
let ident_pat = match param.pat()? {
ast::Pat::IdentPat(it) => it,
_ => return None,
};
let func = param.syntax().ancestors().find_map(ast::Fn::cast)?;
let param_position = func.param_list()?.params().position(|it| it == param)?;
let fn_def = {
let func = ctx.sema.to_def(&func)?;
Definition::ModuleDef(func.into())
};
let param_def = {
let local = ctx.sema.to_def(&ident_pat)?;
Definition::Local(local)
};
if param_def.usages(&ctx.sema).at_least_one() {
mark::hit!(keep_used);
return None;
}
acc.add(
AssistId("remove_unused_param", AssistKind::Refactor),
"Remove unused parameter",
param.syntax().text_range(),
|builder| {
builder.delete(range_with_coma(param.syntax()));
for usage in fn_def.usages(&ctx.sema).all() {
process_usage(ctx, builder, usage, param_position);
}
},
)
}
fn process_usage(
ctx: &AssistContext,
builder: &mut AssistBuilder,
usage: Reference,
arg_to_remove: usize,
) -> Option<()> {
let source_file = ctx.sema.parse(usage.file_range.file_id);
let call_expr: ast::CallExpr =
find_node_at_range(source_file.syntax(), usage.file_range.range)?;
if call_expr.expr()?.syntax().text_range() != usage.file_range.range {
return None;
}
let arg = call_expr.arg_list()?.args().nth(arg_to_remove)?;
builder.edit_file(usage.file_range.file_id);
builder.delete(range_with_coma(arg.syntax()));
Some(())
}
fn range_with_coma(node: &SyntaxNode) -> TextRange {
let up_to = next_prev().find_map(|dir| {
node.siblings_with_tokens(dir)
.filter_map(|it| it.into_token())
.find(|it| it.kind() == T![,])
});
let up_to = up_to.map_or(node.text_range(), |it| it.text_range());
node.text_range().cover(up_to)
}
#[cfg(test)]
mod tests {
use crate::tests::{check_assist, check_assist_not_applicable};
use super::*;
#[test]
fn remove_unused() {
check_assist(
remove_unused_param,
r#"
fn a() { foo(9, 2) }
fn foo(x: i32, <|>y: i32) { x; }
fn b() { foo(9, 2,) }
"#,
r#"
fn a() { foo(9) }
fn foo(x: i32) { x; }
fn b() { foo(9, ) }
"#,
);
}
#[test]
fn keep_used() {
mark::check!(keep_used);
check_assist_not_applicable(
remove_unused_param,
r#"
fn foo(x: i32, <|>y: i32) { y; }
fn main() { foo(9, 2) }
"#,
);
}
}

View file

@ -0,0 +1,220 @@
use itertools::Itertools;
use rustc_hash::FxHashMap;
use hir::{Adt, ModuleDef, PathResolution, Semantics, Struct};
use ide_db::RootDatabase;
use syntax::{algo, ast, match_ast, AstNode, SyntaxKind, SyntaxKind::*, SyntaxNode};
use crate::{AssistContext, AssistId, AssistKind, Assists};
// Assist: reorder_fields
//
// Reorder the fields of record literals and record patterns in the same order as in
// the definition.
//
// ```
// struct Foo {foo: i32, bar: i32};
// const test: Foo = <|>Foo {bar: 0, foo: 1}
// ```
// ->
// ```
// struct Foo {foo: i32, bar: i32};
// const test: Foo = Foo {foo: 1, bar: 0}
// ```
//
pub(crate) fn reorder_fields(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
reorder::<ast::RecordExpr>(acc, ctx).or_else(|| reorder::<ast::RecordPat>(acc, ctx))
}
fn reorder<R: AstNode>(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
let record = ctx.find_node_at_offset::<R>()?;
let path = record.syntax().children().find_map(ast::Path::cast)?;
let ranks = compute_fields_ranks(&path, &ctx)?;
let fields = get_fields(&record.syntax());
let sorted_fields = sorted_by_rank(&fields, |node| {
*ranks.get(&get_field_name(node)).unwrap_or(&usize::max_value())
});
if sorted_fields == fields {
return None;
}
let target = record.syntax().text_range();
acc.add(
AssistId("reorder_fields", AssistKind::RefactorRewrite),
"Reorder record fields",
target,
|edit| {
for (old, new) in fields.iter().zip(&sorted_fields) {
algo::diff(old, new).into_text_edit(edit.text_edit_builder());
}
},
)
}
fn get_fields_kind(node: &SyntaxNode) -> Vec<SyntaxKind> {
match node.kind() {
RECORD_EXPR => vec![RECORD_EXPR_FIELD],
RECORD_PAT => vec![RECORD_PAT_FIELD, IDENT_PAT],
_ => vec![],
}
}
fn get_field_name(node: &SyntaxNode) -> String {
let res = match_ast! {
match node {
ast::RecordExprField(field) => field.field_name().map(|it| it.to_string()),
ast::RecordPatField(field) => field.field_name().map(|it| it.to_string()),
_ => None,
}
};
res.unwrap_or_default()
}
fn get_fields(record: &SyntaxNode) -> Vec<SyntaxNode> {
let kinds = get_fields_kind(record);
record.children().flat_map(|n| n.children()).filter(|n| kinds.contains(&n.kind())).collect()
}
fn sorted_by_rank(
fields: &[SyntaxNode],
get_rank: impl Fn(&SyntaxNode) -> usize,
) -> Vec<SyntaxNode> {
fields.iter().cloned().sorted_by_key(get_rank).collect()
}
fn struct_definition(path: &ast::Path, sema: &Semantics<RootDatabase>) -> Option<Struct> {
match sema.resolve_path(path) {
Some(PathResolution::Def(ModuleDef::Adt(Adt::Struct(s)))) => Some(s),
_ => None,
}
}
fn compute_fields_ranks(path: &ast::Path, ctx: &AssistContext) -> Option<FxHashMap<String, usize>> {
Some(
struct_definition(path, &ctx.sema)?
.fields(ctx.db())
.iter()
.enumerate()
.map(|(idx, field)| (field.name(ctx.db()).to_string(), idx))
.collect(),
)
}
#[cfg(test)]
mod tests {
use crate::tests::{check_assist, check_assist_not_applicable};
use super::*;
#[test]
fn not_applicable_if_sorted() {
check_assist_not_applicable(
reorder_fields,
r#"
struct Foo {
foo: i32,
bar: i32,
}
const test: Foo = <|>Foo { foo: 0, bar: 0 };
"#,
)
}
#[test]
fn trivial_empty_fields() {
check_assist_not_applicable(
reorder_fields,
r#"
struct Foo {};
const test: Foo = <|>Foo {}
"#,
)
}
#[test]
fn reorder_struct_fields() {
check_assist(
reorder_fields,
r#"
struct Foo {foo: i32, bar: i32};
const test: Foo = <|>Foo {bar: 0, foo: 1}
"#,
r#"
struct Foo {foo: i32, bar: i32};
const test: Foo = Foo {foo: 1, bar: 0}
"#,
)
}
#[test]
fn reorder_struct_pattern() {
check_assist(
reorder_fields,
r#"
struct Foo { foo: i64, bar: i64, baz: i64 }
fn f(f: Foo) -> {
match f {
<|>Foo { baz: 0, ref mut bar, .. } => (),
_ => ()
}
}
"#,
r#"
struct Foo { foo: i64, bar: i64, baz: i64 }
fn f(f: Foo) -> {
match f {
Foo { ref mut bar, baz: 0, .. } => (),
_ => ()
}
}
"#,
)
}
#[test]
fn reorder_with_extra_field() {
check_assist(
reorder_fields,
r#"
struct Foo {
foo: String,
bar: String,
}
impl Foo {
fn new() -> Foo {
let foo = String::new();
<|>Foo {
bar: foo.clone(),
extra: "Extra field",
foo,
}
}
}
"#,
r#"
struct Foo {
foo: String,
bar: String,
}
impl Foo {
fn new() -> Foo {
let foo = String::new();
Foo {
foo,
bar: foo.clone(),
extra: "Extra field",
}
}
}
"#,
)
}
}

View file

@ -0,0 +1,257 @@
use syntax::{
ast::{
self,
edit::{AstNodeEdit, IndentLevel},
make,
},
AstNode,
};
use crate::{
utils::{unwrap_trivial_block, TryEnum},
AssistContext, AssistId, AssistKind, Assists,
};
// Assist: replace_if_let_with_match
//
// Replaces `if let` with an else branch with a `match` expression.
//
// ```
// enum Action { Move { distance: u32 }, Stop }
//
// fn handle(action: Action) {
// <|>if let Action::Move { distance } = action {
// foo(distance)
// } else {
// bar()
// }
// }
// ```
// ->
// ```
// enum Action { Move { distance: u32 }, Stop }
//
// fn handle(action: Action) {
// match action {
// Action::Move { distance } => foo(distance),
// _ => bar(),
// }
// }
// ```
pub(crate) fn replace_if_let_with_match(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
let if_expr: ast::IfExpr = ctx.find_node_at_offset()?;
let cond = if_expr.condition()?;
let pat = cond.pat()?;
let expr = cond.expr()?;
let then_block = if_expr.then_branch()?;
let else_block = match if_expr.else_branch()? {
ast::ElseBranch::Block(it) => it,
ast::ElseBranch::IfExpr(_) => return None,
};
let target = if_expr.syntax().text_range();
acc.add(
AssistId("replace_if_let_with_match", AssistKind::RefactorRewrite),
"Replace with match",
target,
move |edit| {
let match_expr = {
let then_arm = {
let then_block = then_block.reset_indent().indent(IndentLevel(1));
let then_expr = unwrap_trivial_block(then_block);
make::match_arm(vec![pat.clone()], then_expr)
};
let else_arm = {
let pattern = ctx
.sema
.type_of_pat(&pat)
.and_then(|ty| TryEnum::from_ty(&ctx.sema, &ty))
.map(|it| it.sad_pattern())
.unwrap_or_else(|| make::wildcard_pat().into());
let else_expr = unwrap_trivial_block(else_block);
make::match_arm(vec![pattern], else_expr)
};
let match_expr =
make::expr_match(expr, make::match_arm_list(vec![then_arm, else_arm]));
match_expr.indent(IndentLevel::from_node(if_expr.syntax()))
};
edit.replace_ast::<ast::Expr>(if_expr.into(), match_expr);
},
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tests::{check_assist, check_assist_target};
#[test]
fn test_replace_if_let_with_match_unwraps_simple_expressions() {
check_assist(
replace_if_let_with_match,
r#"
impl VariantData {
pub fn is_struct(&self) -> bool {
if <|>let VariantData::Struct(..) = *self {
true
} else {
false
}
}
} "#,
r#"
impl VariantData {
pub fn is_struct(&self) -> bool {
match *self {
VariantData::Struct(..) => true,
_ => false,
}
}
} "#,
)
}
#[test]
fn test_replace_if_let_with_match_doesnt_unwrap_multiline_expressions() {
check_assist(
replace_if_let_with_match,
r#"
fn foo() {
if <|>let VariantData::Struct(..) = a {
bar(
123
)
} else {
false
}
} "#,
r#"
fn foo() {
match a {
VariantData::Struct(..) => {
bar(
123
)
}
_ => false,
}
} "#,
)
}
#[test]
fn replace_if_let_with_match_target() {
check_assist_target(
replace_if_let_with_match,
r#"
impl VariantData {
pub fn is_struct(&self) -> bool {
if <|>let VariantData::Struct(..) = *self {
true
} else {
false
}
}
} "#,
"if let VariantData::Struct(..) = *self {
true
} else {
false
}",
);
}
#[test]
fn special_case_option() {
check_assist(
replace_if_let_with_match,
r#"
enum Option<T> { Some(T), None }
use Option::*;
fn foo(x: Option<i32>) {
<|>if let Some(x) = x {
println!("{}", x)
} else {
println!("none")
}
}
"#,
r#"
enum Option<T> { Some(T), None }
use Option::*;
fn foo(x: Option<i32>) {
match x {
Some(x) => println!("{}", x),
None => println!("none"),
}
}
"#,
);
}
#[test]
fn special_case_result() {
check_assist(
replace_if_let_with_match,
r#"
enum Result<T, E> { Ok(T), Err(E) }
use Result::*;
fn foo(x: Result<i32, ()>) {
<|>if let Ok(x) = x {
println!("{}", x)
} else {
println!("none")
}
}
"#,
r#"
enum Result<T, E> { Ok(T), Err(E) }
use Result::*;
fn foo(x: Result<i32, ()>) {
match x {
Ok(x) => println!("{}", x),
Err(_) => println!("none"),
}
}
"#,
);
}
#[test]
fn nested_indent() {
check_assist(
replace_if_let_with_match,
r#"
fn main() {
if true {
<|>if let Ok(rel_path) = path.strip_prefix(root_path) {
let rel_path = RelativePathBuf::from_path(rel_path).ok()?;
Some((*id, rel_path))
} else {
None
}
}
}
"#,
r#"
fn main() {
if true {
match path.strip_prefix(root_path) {
Ok(rel_path) => {
let rel_path = RelativePathBuf::from_path(rel_path).ok()?;
Some((*id, rel_path))
}
_ => None,
}
}
}
"#,
)
}
}

View file

@ -0,0 +1,100 @@
use std::iter::once;
use syntax::{
ast::{
self,
edit::{AstNodeEdit, IndentLevel},
make,
},
AstNode, T,
};
use crate::{utils::TryEnum, AssistContext, AssistId, AssistKind, Assists};
// Assist: replace_let_with_if_let
//
// Replaces `let` with an `if-let`.
//
// ```
// # enum Option<T> { Some(T), None }
//
// fn main(action: Action) {
// <|>let x = compute();
// }
//
// fn compute() -> Option<i32> { None }
// ```
// ->
// ```
// # enum Option<T> { Some(T), None }
//
// fn main(action: Action) {
// if let Some(x) = compute() {
// }
// }
//
// fn compute() -> Option<i32> { None }
// ```
pub(crate) fn replace_let_with_if_let(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
let let_kw = ctx.find_token_at_offset(T![let])?;
let let_stmt = let_kw.ancestors().find_map(ast::LetStmt::cast)?;
let init = let_stmt.initializer()?;
let original_pat = let_stmt.pat()?;
let ty = ctx.sema.type_of_expr(&init)?;
let happy_variant = TryEnum::from_ty(&ctx.sema, &ty).map(|it| it.happy_case());
let target = let_kw.text_range();
acc.add(
AssistId("replace_let_with_if_let", AssistKind::RefactorRewrite),
"Replace with if-let",
target,
|edit| {
let with_placeholder: ast::Pat = match happy_variant {
None => make::wildcard_pat().into(),
Some(var_name) => make::tuple_struct_pat(
make::path_unqualified(make::path_segment(make::name_ref(var_name))),
once(make::wildcard_pat().into()),
)
.into(),
};
let block =
make::block_expr(None, None).indent(IndentLevel::from_node(let_stmt.syntax()));
let if_ = make::expr_if(make::condition(init, Some(with_placeholder)), block);
let stmt = make::expr_stmt(if_);
let placeholder = stmt.syntax().descendants().find_map(ast::WildcardPat::cast).unwrap();
let stmt = stmt.replace_descendant(placeholder.into(), original_pat);
edit.replace_ast(ast::Stmt::from(let_stmt), ast::Stmt::from(stmt));
},
)
}
#[cfg(test)]
mod tests {
use crate::tests::check_assist;
use super::*;
#[test]
fn replace_let_unknown_enum() {
check_assist(
replace_let_with_if_let,
r"
enum E<T> { X(T), Y(T) }
fn main() {
<|>let x = E::X(92);
}
",
r"
enum E<T> { X(T), Y(T) }
fn main() {
if let x = E::X(92) {
}
}
",
)
}
}

View file

@ -0,0 +1,680 @@
use syntax::{algo::SyntaxRewriter, ast, match_ast, AstNode, SyntaxNode, TextRange};
use test_utils::mark;
use crate::{
utils::{find_insert_use_container, insert_use_statement},
AssistContext, AssistId, AssistKind, Assists,
};
// Assist: replace_qualified_name_with_use
//
// Adds a use statement for a given fully-qualified name.
//
// ```
// fn process(map: std::collections::<|>HashMap<String, String>) {}
// ```
// ->
// ```
// use std::collections::HashMap;
//
// fn process(map: HashMap<String, String>) {}
// ```
pub(crate) fn replace_qualified_name_with_use(
acc: &mut Assists,
ctx: &AssistContext,
) -> Option<()> {
let path: ast::Path = ctx.find_node_at_offset()?;
// We don't want to mess with use statements
if path.syntax().ancestors().find_map(ast::Use::cast).is_some() {
return None;
}
if path.qualifier().is_none() {
mark::hit!(dont_import_trivial_paths);
return None;
}
let path_to_import = path.to_string().clone();
let path_to_import = match path.segment()?.generic_arg_list() {
Some(generic_args) => {
let generic_args_start =
generic_args.syntax().text_range().start() - path.syntax().text_range().start();
&path_to_import[TextRange::up_to(generic_args_start)]
}
None => path_to_import.as_str(),
};
let target = path.syntax().text_range();
acc.add(
AssistId("replace_qualified_name_with_use", AssistKind::RefactorRewrite),
"Replace qualified path with use",
target,
|builder| {
let container = match find_insert_use_container(path.syntax(), ctx) {
Some(c) => c,
None => return,
};
insert_use_statement(
path.syntax(),
&path_to_import.to_string(),
ctx,
builder.text_edit_builder(),
);
// Now that we've brought the name into scope, re-qualify all paths that could be
// affected (that is, all paths inside the node we added the `use` to).
let mut rewriter = SyntaxRewriter::default();
let syntax = container.either(|l| l.syntax().clone(), |r| r.syntax().clone());
shorten_paths(&mut rewriter, syntax, path);
builder.rewrite(rewriter);
},
)
}
/// Adds replacements to `re` that shorten `path` in all descendants of `node`.
fn shorten_paths(rewriter: &mut SyntaxRewriter<'static>, node: SyntaxNode, path: ast::Path) {
for child in node.children() {
match_ast! {
match child {
// Don't modify `use` items, as this can break the `use` item when injecting a new
// import into the use tree.
ast::Use(_it) => continue,
// Don't descend into submodules, they don't have the same `use` items in scope.
ast::Module(_it) => continue,
ast::Path(p) => {
match maybe_replace_path(rewriter, p.clone(), path.clone()) {
Some(()) => {},
None => shorten_paths(rewriter, p.syntax().clone(), path.clone()),
}
},
_ => shorten_paths(rewriter, child, path.clone()),
}
}
}
}
fn maybe_replace_path(
rewriter: &mut SyntaxRewriter<'static>,
path: ast::Path,
target: ast::Path,
) -> Option<()> {
if !path_eq(path.clone(), target) {
return None;
}
// Shorten `path`, leaving only its last segment.
if let Some(parent) = path.qualifier() {
rewriter.delete(parent.syntax());
}
if let Some(double_colon) = path.coloncolon_token() {
rewriter.delete(&double_colon);
}
Some(())
}
fn path_eq(lhs: ast::Path, rhs: ast::Path) -> bool {
let mut lhs_curr = lhs;
let mut rhs_curr = rhs;
loop {
match (lhs_curr.segment(), rhs_curr.segment()) {
(Some(lhs), Some(rhs)) if lhs.syntax().text() == rhs.syntax().text() => (),
_ => return false,
}
match (lhs_curr.qualifier(), rhs_curr.qualifier()) {
(Some(lhs), Some(rhs)) => {
lhs_curr = lhs;
rhs_curr = rhs;
}
(None, None) => return true,
_ => return false,
}
}
}
#[cfg(test)]
mod tests {
use crate::tests::{check_assist, check_assist_not_applicable};
use super::*;
#[test]
fn test_replace_add_use_no_anchor() {
check_assist(
replace_qualified_name_with_use,
r"
std::fmt::Debug<|>
",
r"
use std::fmt::Debug;
Debug
",
);
}
#[test]
fn test_replace_add_use_no_anchor_with_item_below() {
check_assist(
replace_qualified_name_with_use,
r"
std::fmt::Debug<|>
fn main() {
}
",
r"
use std::fmt::Debug;
Debug
fn main() {
}
",
);
}
#[test]
fn test_replace_add_use_no_anchor_with_item_above() {
check_assist(
replace_qualified_name_with_use,
r"
fn main() {
}
std::fmt::Debug<|>
",
r"
use std::fmt::Debug;
fn main() {
}
Debug
",
);
}
#[test]
fn test_replace_add_use_no_anchor_2seg() {
check_assist(
replace_qualified_name_with_use,
r"
std::fmt<|>::Debug
",
r"
use std::fmt;
fmt::Debug
",
);
}
#[test]
fn test_replace_add_use() {
check_assist(
replace_qualified_name_with_use,
r"
use stdx;
impl std::fmt::Debug<|> for Foo {
}
",
r"
use stdx;
use std::fmt::Debug;
impl Debug for Foo {
}
",
);
}
#[test]
fn test_replace_file_use_other_anchor() {
check_assist(
replace_qualified_name_with_use,
r"
impl std::fmt::Debug<|> for Foo {
}
",
r"
use std::fmt::Debug;
impl Debug for Foo {
}
",
);
}
#[test]
fn test_replace_add_use_other_anchor_indent() {
check_assist(
replace_qualified_name_with_use,
r"
impl std::fmt::Debug<|> for Foo {
}
",
r"
use std::fmt::Debug;
impl Debug for Foo {
}
",
);
}
#[test]
fn test_replace_split_different() {
check_assist(
replace_qualified_name_with_use,
r"
use std::fmt;
impl std::io<|> for Foo {
}
",
r"
use std::{io, fmt};
impl io for Foo {
}
",
);
}
#[test]
fn test_replace_split_self_for_use() {
check_assist(
replace_qualified_name_with_use,
r"
use std::fmt;
impl std::fmt::Debug<|> for Foo {
}
",
r"
use std::fmt::{self, Debug, };
impl Debug for Foo {
}
",
);
}
#[test]
fn test_replace_split_self_for_target() {
check_assist(
replace_qualified_name_with_use,
r"
use std::fmt::Debug;
impl std::fmt<|> for Foo {
}
",
r"
use std::fmt::{self, Debug};
impl fmt for Foo {
}
",
);
}
#[test]
fn test_replace_add_to_nested_self_nested() {
check_assist(
replace_qualified_name_with_use,
r"
use std::fmt::{Debug, nested::{Display}};
impl std::fmt::nested<|> for Foo {
}
",
r"
use std::fmt::{Debug, nested::{Display, self}};
impl nested for Foo {
}
",
);
}
#[test]
fn test_replace_add_to_nested_self_already_included() {
check_assist(
replace_qualified_name_with_use,
r"
use std::fmt::{Debug, nested::{self, Display}};
impl std::fmt::nested<|> for Foo {
}
",
r"
use std::fmt::{Debug, nested::{self, Display}};
impl nested for Foo {
}
",
);
}
#[test]
fn test_replace_add_to_nested_nested() {
check_assist(
replace_qualified_name_with_use,
r"
use std::fmt::{Debug, nested::{Display}};
impl std::fmt::nested::Debug<|> for Foo {
}
",
r"
use std::fmt::{Debug, nested::{Display, Debug}};
impl Debug for Foo {
}
",
);
}
#[test]
fn test_replace_split_common_target_longer() {
check_assist(
replace_qualified_name_with_use,
r"
use std::fmt::Debug;
impl std::fmt::nested::Display<|> for Foo {
}
",
r"
use std::fmt::{nested::Display, Debug};
impl Display for Foo {
}
",
);
}
#[test]
fn test_replace_split_common_use_longer() {
check_assist(
replace_qualified_name_with_use,
r"
use std::fmt::nested::Debug;
impl std::fmt::Display<|> for Foo {
}
",
r"
use std::fmt::{Display, nested::Debug};
impl Display for Foo {
}
",
);
}
#[test]
fn test_replace_use_nested_import() {
check_assist(
replace_qualified_name_with_use,
r"
use crate::{
ty::{Substs, Ty},
AssocItem,
};
fn foo() { crate::ty::lower<|>::trait_env() }
",
r"
use crate::{
ty::{Substs, Ty, lower},
AssocItem,
};
fn foo() { lower::trait_env() }
",
);
}
#[test]
fn test_replace_alias() {
check_assist(
replace_qualified_name_with_use,
r"
use std::fmt as foo;
impl foo::Debug<|> for Foo {
}
",
r"
use std::fmt as foo;
impl Debug for Foo {
}
",
);
}
#[test]
fn dont_import_trivial_paths() {
mark::check!(dont_import_trivial_paths);
check_assist_not_applicable(
replace_qualified_name_with_use,
r"
impl foo<|> for Foo {
}
",
);
}
#[test]
fn test_replace_not_applicable_in_use() {
check_assist_not_applicable(
replace_qualified_name_with_use,
r"
use std::fmt<|>;
",
);
}
#[test]
fn test_replace_add_use_no_anchor_in_mod_mod() {
check_assist(
replace_qualified_name_with_use,
r"
mod foo {
mod bar {
std::fmt::Debug<|>
}
}
",
r"
mod foo {
mod bar {
use std::fmt::Debug;
Debug
}
}
",
);
}
#[test]
fn inserts_imports_after_inner_attributes() {
check_assist(
replace_qualified_name_with_use,
r"
#![allow(dead_code)]
fn main() {
std::fmt::Debug<|>
}
",
r"
#![allow(dead_code)]
use std::fmt::Debug;
fn main() {
Debug
}
",
);
}
#[test]
fn replaces_all_affected_paths() {
check_assist(
replace_qualified_name_with_use,
r"
fn main() {
std::fmt::Debug<|>;
let x: std::fmt::Debug = std::fmt::Debug;
}
",
r"
use std::fmt::Debug;
fn main() {
Debug;
let x: Debug = Debug;
}
",
);
}
#[test]
fn replaces_all_affected_paths_mod() {
check_assist(
replace_qualified_name_with_use,
r"
mod m {
fn f() {
std::fmt::Debug<|>;
let x: std::fmt::Debug = std::fmt::Debug;
}
fn g() {
std::fmt::Debug;
}
}
fn f() {
std::fmt::Debug;
}
",
r"
mod m {
use std::fmt::Debug;
fn f() {
Debug;
let x: Debug = Debug;
}
fn g() {
Debug;
}
}
fn f() {
std::fmt::Debug;
}
",
);
}
#[test]
fn does_not_replace_in_submodules() {
check_assist(
replace_qualified_name_with_use,
r"
fn main() {
std::fmt::Debug<|>;
}
mod sub {
fn f() {
std::fmt::Debug;
}
}
",
r"
use std::fmt::Debug;
fn main() {
Debug;
}
mod sub {
fn f() {
std::fmt::Debug;
}
}
",
);
}
#[test]
fn does_not_replace_in_use() {
check_assist(
replace_qualified_name_with_use,
r"
use std::fmt::Display;
fn main() {
std::fmt<|>;
}
",
r"
use std::fmt::{self, Display};
fn main() {
fmt;
}
",
);
}
#[test]
fn does_not_replace_pub_use() {
check_assist(
replace_qualified_name_with_use,
r"
pub use std::fmt;
impl std::io<|> for Foo {
}
",
r"
use std::io;
pub use std::fmt;
impl io for Foo {
}
",
);
}
#[test]
fn does_not_replace_pub_crate_use() {
check_assist(
replace_qualified_name_with_use,
r"
pub(crate) use std::fmt;
impl std::io<|> for Foo {
}
",
r"
use std::io;
pub(crate) use std::fmt;
impl io for Foo {
}
",
);
}
}

View file

@ -0,0 +1,187 @@
use std::iter;
use syntax::{
ast::{
self,
edit::{AstNodeEdit, IndentLevel},
make,
},
AstNode,
};
use crate::{
utils::{render_snippet, Cursor, TryEnum},
AssistContext, AssistId, AssistKind, Assists,
};
// Assist: replace_unwrap_with_match
//
// Replaces `unwrap` a `match` expression. Works for Result and Option.
//
// ```
// enum Result<T, E> { Ok(T), Err(E) }
// fn main() {
// let x: Result<i32, i32> = Result::Ok(92);
// let y = x.<|>unwrap();
// }
// ```
// ->
// ```
// enum Result<T, E> { Ok(T), Err(E) }
// fn main() {
// let x: Result<i32, i32> = Result::Ok(92);
// let y = match x {
// Ok(a) => a,
// $0_ => unreachable!(),
// };
// }
// ```
pub(crate) fn replace_unwrap_with_match(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
let method_call: ast::MethodCallExpr = ctx.find_node_at_offset()?;
let name = method_call.name_ref()?;
if name.text() != "unwrap" {
return None;
}
let caller = method_call.receiver()?;
let ty = ctx.sema.type_of_expr(&caller)?;
let happy_variant = TryEnum::from_ty(&ctx.sema, &ty)?.happy_case();
let target = method_call.syntax().text_range();
acc.add(
AssistId("replace_unwrap_with_match", AssistKind::RefactorRewrite),
"Replace unwrap with match",
target,
|builder| {
let ok_path = make::path_unqualified(make::path_segment(make::name_ref(happy_variant)));
let it = make::ident_pat(make::name("a")).into();
let ok_tuple = make::tuple_struct_pat(ok_path, iter::once(it)).into();
let bind_path = make::path_unqualified(make::path_segment(make::name_ref("a")));
let ok_arm = make::match_arm(iter::once(ok_tuple), make::expr_path(bind_path));
let unreachable_call = make::expr_unreachable();
let err_arm =
make::match_arm(iter::once(make::wildcard_pat().into()), unreachable_call);
let match_arm_list = make::match_arm_list(vec![ok_arm, err_arm]);
let match_expr = make::expr_match(caller.clone(), match_arm_list)
.indent(IndentLevel::from_node(method_call.syntax()));
let range = method_call.syntax().text_range();
match ctx.config.snippet_cap {
Some(cap) => {
let err_arm = match_expr
.syntax()
.descendants()
.filter_map(ast::MatchArm::cast)
.last()
.unwrap();
let snippet =
render_snippet(cap, match_expr.syntax(), Cursor::Before(err_arm.syntax()));
builder.replace_snippet(cap, range, snippet)
}
None => builder.replace(range, match_expr.to_string()),
}
},
)
}
#[cfg(test)]
mod tests {
use crate::tests::{check_assist, check_assist_target};
use super::*;
#[test]
fn test_replace_result_unwrap_with_match() {
check_assist(
replace_unwrap_with_match,
r"
enum Result<T, E> { Ok(T), Err(E) }
fn i<T>(a: T) -> T { a }
fn main() {
let x: Result<i32, i32> = Result::Ok(92);
let y = i(x).<|>unwrap();
}
",
r"
enum Result<T, E> { Ok(T), Err(E) }
fn i<T>(a: T) -> T { a }
fn main() {
let x: Result<i32, i32> = Result::Ok(92);
let y = match i(x) {
Ok(a) => a,
$0_ => unreachable!(),
};
}
",
)
}
#[test]
fn test_replace_option_unwrap_with_match() {
check_assist(
replace_unwrap_with_match,
r"
enum Option<T> { Some(T), None }
fn i<T>(a: T) -> T { a }
fn main() {
let x = Option::Some(92);
let y = i(x).<|>unwrap();
}
",
r"
enum Option<T> { Some(T), None }
fn i<T>(a: T) -> T { a }
fn main() {
let x = Option::Some(92);
let y = match i(x) {
Some(a) => a,
$0_ => unreachable!(),
};
}
",
);
}
#[test]
fn test_replace_result_unwrap_with_match_chaining() {
check_assist(
replace_unwrap_with_match,
r"
enum Result<T, E> { Ok(T), Err(E) }
fn i<T>(a: T) -> T { a }
fn main() {
let x: Result<i32, i32> = Result::Ok(92);
let y = i(x).<|>unwrap().count_zeroes();
}
",
r"
enum Result<T, E> { Ok(T), Err(E) }
fn i<T>(a: T) -> T { a }
fn main() {
let x: Result<i32, i32> = Result::Ok(92);
let y = match i(x) {
Ok(a) => a,
$0_ => unreachable!(),
}.count_zeroes();
}
",
)
}
#[test]
fn replace_unwrap_with_match_target() {
check_assist_target(
replace_unwrap_with_match,
r"
enum Option<T> { Some(T), None }
fn i<T>(a: T) -> T { a }
fn main() {
let x = Option::Some(92);
let y = i(x).<|>unwrap();
}
",
r"i(x).unwrap()",
);
}
}

View file

@ -0,0 +1,79 @@
use std::iter::successors;
use syntax::{ast, AstNode, T};
use crate::{AssistContext, AssistId, AssistKind, Assists};
// Assist: split_import
//
// Wraps the tail of import into braces.
//
// ```
// use std::<|>collections::HashMap;
// ```
// ->
// ```
// use std::{collections::HashMap};
// ```
pub(crate) fn split_import(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
let colon_colon = ctx.find_token_at_offset(T![::])?;
let path = ast::Path::cast(colon_colon.parent())?.qualifier()?;
let top_path = successors(Some(path.clone()), |it| it.parent_path()).last()?;
let use_tree = top_path.syntax().ancestors().find_map(ast::UseTree::cast)?;
let new_tree = use_tree.split_prefix(&path);
if new_tree == use_tree {
return None;
}
let target = colon_colon.text_range();
acc.add(AssistId("split_import", AssistKind::RefactorRewrite), "Split import", target, |edit| {
edit.replace_ast(use_tree, new_tree);
})
}
#[cfg(test)]
mod tests {
use crate::tests::{check_assist, check_assist_not_applicable, check_assist_target};
use super::*;
#[test]
fn test_split_import() {
check_assist(
split_import,
"use crate::<|>db::RootDatabase;",
"use crate::{db::RootDatabase};",
)
}
#[test]
fn split_import_works_with_trees() {
check_assist(
split_import,
"use crate:<|>:db::{RootDatabase, FileSymbol}",
"use crate::{db::{RootDatabase, FileSymbol}}",
)
}
#[test]
fn split_import_target() {
check_assist_target(split_import, "use crate::<|>db::{RootDatabase, FileSymbol}", "::");
}
#[test]
fn issue4044() {
check_assist_not_applicable(split_import, "use crate::<|>:::self;")
}
#[test]
fn test_empty_use() {
check_assist_not_applicable(
split_import,
r"
use std::<|>
fn main() {}",
);
}
}

View file

@ -0,0 +1,517 @@
use syntax::{
ast::{
self,
edit::{AstNodeEdit, IndentLevel},
},
AstNode, TextRange, T,
};
use crate::{utils::unwrap_trivial_block, AssistContext, AssistId, AssistKind, Assists};
// Assist: unwrap_block
//
// This assist removes if...else, for, while and loop control statements to just keep the body.
//
// ```
// fn foo() {
// if true {<|>
// println!("foo");
// }
// }
// ```
// ->
// ```
// fn foo() {
// println!("foo");
// }
// ```
pub(crate) fn unwrap_block(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
let assist_id = AssistId("unwrap_block", AssistKind::RefactorRewrite);
let assist_label = "Unwrap block";
let l_curly_token = ctx.find_token_at_offset(T!['{'])?;
let mut block = ast::BlockExpr::cast(l_curly_token.parent())?;
let mut parent = block.syntax().parent()?;
if ast::MatchArm::can_cast(parent.kind()) {
parent = parent.ancestors().find(|it| ast::MatchExpr::can_cast(it.kind()))?
}
let parent = ast::Expr::cast(parent)?;
match parent.clone() {
ast::Expr::ForExpr(_) | ast::Expr::WhileExpr(_) | ast::Expr::LoopExpr(_) => (),
ast::Expr::MatchExpr(_) => block = block.dedent(IndentLevel(1)),
ast::Expr::IfExpr(if_expr) => {
let then_branch = if_expr.then_branch()?;
if then_branch == block {
if let Some(ancestor) = if_expr.syntax().parent().and_then(ast::IfExpr::cast) {
// For `else if` blocks
let ancestor_then_branch = ancestor.then_branch()?;
let target = then_branch.syntax().text_range();
return acc.add(assist_id, assist_label, target, |edit| {
let range_to_del_else_if = TextRange::new(
ancestor_then_branch.syntax().text_range().end(),
l_curly_token.text_range().start(),
);
let range_to_del_rest = TextRange::new(
then_branch.syntax().text_range().end(),
if_expr.syntax().text_range().end(),
);
edit.delete(range_to_del_rest);
edit.delete(range_to_del_else_if);
edit.replace(
target,
update_expr_string(then_branch.to_string(), &[' ', '{']),
);
});
}
} else {
let target = block.syntax().text_range();
return acc.add(assist_id, assist_label, target, |edit| {
let range_to_del = TextRange::new(
then_branch.syntax().text_range().end(),
l_curly_token.text_range().start(),
);
edit.delete(range_to_del);
edit.replace(target, update_expr_string(block.to_string(), &[' ', '{']));
});
}
}
_ => return None,
};
let unwrapped = unwrap_trivial_block(block);
let target = unwrapped.syntax().text_range();
acc.add(assist_id, assist_label, target, |builder| {
builder.replace(
parent.syntax().text_range(),
update_expr_string(unwrapped.to_string(), &[' ', '{', '\n']),
);
})
}
fn update_expr_string(expr_str: String, trim_start_pat: &[char]) -> String {
let expr_string = expr_str.trim_start_matches(trim_start_pat);
let mut expr_string_lines: Vec<&str> = expr_string.lines().collect();
expr_string_lines.pop(); // Delete last line
expr_string_lines
.into_iter()
.map(|line| line.replacen(" ", "", 1)) // Delete indentation
.collect::<Vec<String>>()
.join("\n")
}
#[cfg(test)]
mod tests {
use crate::tests::{check_assist, check_assist_not_applicable};
use super::*;
#[test]
fn simple_if() {
check_assist(
unwrap_block,
r#"
fn main() {
bar();
if true {<|>
foo();
//comment
bar();
} else {
println!("bar");
}
}
"#,
r#"
fn main() {
bar();
foo();
//comment
bar();
}
"#,
);
}
#[test]
fn simple_if_else() {
check_assist(
unwrap_block,
r#"
fn main() {
bar();
if true {
foo();
//comment
bar();
} else {<|>
println!("bar");
}
}
"#,
r#"
fn main() {
bar();
if true {
foo();
//comment
bar();
}
println!("bar");
}
"#,
);
}
#[test]
fn simple_if_else_if() {
check_assist(
unwrap_block,
r#"
fn main() {
//bar();
if true {
println!("true");
//comment
//bar();
} else if false {<|>
println!("bar");
} else {
println!("foo");
}
}
"#,
r#"
fn main() {
//bar();
if true {
println!("true");
//comment
//bar();
}
println!("bar");
}
"#,
);
}
#[test]
fn simple_if_else_if_nested() {
check_assist(
unwrap_block,
r#"
fn main() {
//bar();
if true {
println!("true");
//comment
//bar();
} else if false {
println!("bar");
} else if true {<|>
println!("foo");
}
}
"#,
r#"
fn main() {
//bar();
if true {
println!("true");
//comment
//bar();
} else if false {
println!("bar");
}
println!("foo");
}
"#,
);
}
#[test]
fn simple_if_else_if_nested_else() {
check_assist(
unwrap_block,
r#"
fn main() {
//bar();
if true {
println!("true");
//comment
//bar();
} else if false {
println!("bar");
} else if true {
println!("foo");
} else {<|>
println!("else");
}
}
"#,
r#"
fn main() {
//bar();
if true {
println!("true");
//comment
//bar();
} else if false {
println!("bar");
} else if true {
println!("foo");
}
println!("else");
}
"#,
);
}
#[test]
fn simple_if_else_if_nested_middle() {
check_assist(
unwrap_block,
r#"
fn main() {
//bar();
if true {
println!("true");
//comment
//bar();
} else if false {
println!("bar");
} else if true {<|>
println!("foo");
} else {
println!("else");
}
}
"#,
r#"
fn main() {
//bar();
if true {
println!("true");
//comment
//bar();
} else if false {
println!("bar");
}
println!("foo");
}
"#,
);
}
#[test]
fn simple_if_bad_cursor_position() {
check_assist_not_applicable(
unwrap_block,
r#"
fn main() {
bar();<|>
if true {
foo();
//comment
bar();
} else {
println!("bar");
}
}
"#,
);
}
#[test]
fn simple_for() {
check_assist(
unwrap_block,
r#"
fn main() {
for i in 0..5 {<|>
if true {
foo();
//comment
bar();
} else {
println!("bar");
}
}
}
"#,
r#"
fn main() {
if true {
foo();
//comment
bar();
} else {
println!("bar");
}
}
"#,
);
}
#[test]
fn simple_if_in_for() {
check_assist(
unwrap_block,
r#"
fn main() {
for i in 0..5 {
if true {<|>
foo();
//comment
bar();
} else {
println!("bar");
}
}
}
"#,
r#"
fn main() {
for i in 0..5 {
foo();
//comment
bar();
}
}
"#,
);
}
#[test]
fn simple_loop() {
check_assist(
unwrap_block,
r#"
fn main() {
loop {<|>
if true {
foo();
//comment
bar();
} else {
println!("bar");
}
}
}
"#,
r#"
fn main() {
if true {
foo();
//comment
bar();
} else {
println!("bar");
}
}
"#,
);
}
#[test]
fn simple_while() {
check_assist(
unwrap_block,
r#"
fn main() {
while true {<|>
if true {
foo();
//comment
bar();
} else {
println!("bar");
}
}
}
"#,
r#"
fn main() {
if true {
foo();
//comment
bar();
} else {
println!("bar");
}
}
"#,
);
}
#[test]
fn unwrap_match_arm() {
check_assist(
unwrap_block,
r#"
fn main() {
match rel_path {
Ok(rel_path) => {<|>
let rel_path = RelativePathBuf::from_path(rel_path).ok()?;
Some((*id, rel_path))
}
Err(_) => None,
}
}
"#,
r#"
fn main() {
let rel_path = RelativePathBuf::from_path(rel_path).ok()?;
Some((*id, rel_path))
}
"#,
);
}
#[test]
fn simple_if_in_while_bad_cursor_position() {
check_assist_not_applicable(
unwrap_block,
r#"
fn main() {
while true {
if true {
foo();<|>
//comment
bar();
} else {
println!("bar");
}
}
}
"#,
);
}
}

217
crates/assists/src/lib.rs Normal file
View file

@ -0,0 +1,217 @@
//! `assists` crate provides a bunch of code assists, also known as code
//! actions (in LSP) or intentions (in IntelliJ).
//!
//! An assist is a micro-refactoring, which is automatically activated in
//! certain context. For example, if the cursor is over `,`, a "swap `,`" assist
//! becomes available.
#[allow(unused)]
macro_rules! eprintln {
($($tt:tt)*) => { stdx::eprintln!($($tt)*) };
}
mod assist_config;
mod assist_context;
#[cfg(test)]
mod tests;
pub mod utils;
pub mod ast_transform;
use base_db::FileRange;
use hir::Semantics;
use ide_db::{label::Label, source_change::SourceChange, RootDatabase};
use syntax::TextRange;
pub(crate) use crate::assist_context::{AssistContext, Assists};
pub use assist_config::AssistConfig;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AssistKind {
None,
QuickFix,
Generate,
Refactor,
RefactorExtract,
RefactorInline,
RefactorRewrite,
}
impl AssistKind {
pub fn contains(self, other: AssistKind) -> bool {
if self == other {
return true;
}
match self {
AssistKind::None | AssistKind::Generate => return true,
AssistKind::Refactor => match other {
AssistKind::RefactorExtract
| AssistKind::RefactorInline
| AssistKind::RefactorRewrite => return true,
_ => return false,
},
_ => return false,
}
}
}
/// Unique identifier of the assist, should not be shown to the user
/// directly.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AssistId(pub &'static str, pub AssistKind);
#[derive(Clone, Debug)]
pub struct GroupLabel(pub String);
#[derive(Debug, Clone)]
pub struct Assist {
pub id: AssistId,
/// Short description of the assist, as shown in the UI.
pub label: Label,
pub group: Option<GroupLabel>,
/// Target ranges are used to sort assists: the smaller the target range,
/// the more specific assist is, and so it should be sorted first.
pub target: TextRange,
}
#[derive(Debug, Clone)]
pub struct ResolvedAssist {
pub assist: Assist,
pub source_change: SourceChange,
}
impl Assist {
/// Return all the assists applicable at the given position.
///
/// Assists are returned in the "unresolved" state, that is only labels are
/// returned, without actual edits.
pub fn unresolved(db: &RootDatabase, config: &AssistConfig, range: FileRange) -> Vec<Assist> {
let sema = Semantics::new(db);
let ctx = AssistContext::new(sema, config, range);
let mut acc = Assists::new_unresolved(&ctx);
handlers::all().iter().for_each(|handler| {
handler(&mut acc, &ctx);
});
acc.finish_unresolved()
}
/// Return all the assists applicable at the given position.
///
/// Assists are returned in the "resolved" state, that is with edit fully
/// computed.
pub fn resolved(
db: &RootDatabase,
config: &AssistConfig,
range: FileRange,
) -> Vec<ResolvedAssist> {
let sema = Semantics::new(db);
let ctx = AssistContext::new(sema, config, range);
let mut acc = Assists::new_resolved(&ctx);
handlers::all().iter().for_each(|handler| {
handler(&mut acc, &ctx);
});
acc.finish_resolved()
}
}
mod handlers {
use crate::{AssistContext, Assists};
pub(crate) type Handler = fn(&mut Assists, &AssistContext) -> Option<()>;
mod add_custom_impl;
mod add_explicit_type;
mod add_missing_impl_members;
mod add_turbo_fish;
mod apply_demorgan;
mod auto_import;
mod change_return_type_to_result;
mod change_visibility;
mod early_return;
mod expand_glob_import;
mod extract_struct_from_enum_variant;
mod extract_variable;
mod fill_match_arms;
mod fix_visibility;
mod flip_binexpr;
mod flip_comma;
mod flip_trait_bound;
mod generate_derive;
mod generate_from_impl_for_enum;
mod generate_function;
mod generate_impl;
mod generate_new;
mod inline_local_variable;
mod introduce_named_lifetime;
mod invert_if;
mod merge_imports;
mod merge_match_arms;
mod move_bounds;
mod move_guard;
mod raw_string;
mod remove_dbg;
mod remove_mut;
mod remove_unused_param;
mod reorder_fields;
mod replace_if_let_with_match;
mod replace_let_with_if_let;
mod replace_qualified_name_with_use;
mod replace_unwrap_with_match;
mod split_import;
mod unwrap_block;
pub(crate) fn all() -> &'static [Handler] {
&[
// These are alphabetic for the foolish consistency
add_custom_impl::add_custom_impl,
add_explicit_type::add_explicit_type,
add_turbo_fish::add_turbo_fish,
apply_demorgan::apply_demorgan,
auto_import::auto_import,
change_return_type_to_result::change_return_type_to_result,
change_visibility::change_visibility,
early_return::convert_to_guarded_return,
expand_glob_import::expand_glob_import,
extract_struct_from_enum_variant::extract_struct_from_enum_variant,
extract_variable::extract_variable,
fill_match_arms::fill_match_arms,
fix_visibility::fix_visibility,
flip_binexpr::flip_binexpr,
flip_comma::flip_comma,
flip_trait_bound::flip_trait_bound,
generate_derive::generate_derive,
generate_from_impl_for_enum::generate_from_impl_for_enum,
generate_function::generate_function,
generate_impl::generate_impl,
generate_new::generate_new,
inline_local_variable::inline_local_variable,
introduce_named_lifetime::introduce_named_lifetime,
invert_if::invert_if,
merge_imports::merge_imports,
merge_match_arms::merge_match_arms,
move_bounds::move_bounds_to_where_clause,
move_guard::move_arm_cond_to_match_guard,
move_guard::move_guard_to_arm_body,
raw_string::add_hash,
raw_string::make_raw_string,
raw_string::make_usual_string,
raw_string::remove_hash,
remove_dbg::remove_dbg,
remove_mut::remove_mut,
remove_unused_param::remove_unused_param,
reorder_fields::reorder_fields,
replace_if_let_with_match::replace_if_let_with_match,
replace_let_with_if_let::replace_let_with_if_let,
replace_qualified_name_with_use::replace_qualified_name_with_use,
replace_unwrap_with_match::replace_unwrap_with_match,
split_import::split_import,
unwrap_block::unwrap_block,
// These are manually sorted for better priorities
add_missing_impl_members::add_missing_impl_members,
add_missing_impl_members::add_missing_default_members,
// Are you sure you want to add new assist here, and not to the
// sorted list above?
]
}
}

179
crates/assists/src/tests.rs Normal file
View file

@ -0,0 +1,179 @@
mod generated;
use base_db::{fixture::WithFixture, FileId, FileRange, SourceDatabaseExt};
use hir::Semantics;
use ide_db::RootDatabase;
use syntax::TextRange;
use test_utils::{assert_eq_text, extract_offset, extract_range};
use crate::{handlers::Handler, Assist, AssistConfig, AssistContext, AssistKind, Assists};
use stdx::trim_indent;
pub(crate) fn with_single_file(text: &str) -> (RootDatabase, FileId) {
RootDatabase::with_single_file(text)
}
pub(crate) fn check_assist(assist: Handler, ra_fixture_before: &str, ra_fixture_after: &str) {
let ra_fixture_after = trim_indent(ra_fixture_after);
check(assist, ra_fixture_before, ExpectedResult::After(&ra_fixture_after));
}
// FIXME: instead of having a separate function here, maybe use
// `extract_ranges` and mark the target as `<target> </target>` in the
// fixture?
pub(crate) fn check_assist_target(assist: Handler, ra_fixture: &str, target: &str) {
check(assist, ra_fixture, ExpectedResult::Target(target));
}
pub(crate) fn check_assist_not_applicable(assist: Handler, ra_fixture: &str) {
check(assist, ra_fixture, ExpectedResult::NotApplicable);
}
fn check_doc_test(assist_id: &str, before: &str, after: &str) {
let after = trim_indent(after);
let (db, file_id, selection) = RootDatabase::with_range_or_offset(&before);
let before = db.file_text(file_id).to_string();
let frange = FileRange { file_id, range: selection.into() };
let mut assist = Assist::resolved(&db, &AssistConfig::default(), frange)
.into_iter()
.find(|assist| assist.assist.id.0 == assist_id)
.unwrap_or_else(|| {
panic!(
"\n\nAssist is not applicable: {}\nAvailable assists: {}",
assist_id,
Assist::resolved(&db, &AssistConfig::default(), frange)
.into_iter()
.map(|assist| assist.assist.id.0)
.collect::<Vec<_>>()
.join(", ")
)
});
let actual = {
let change = assist.source_change.source_file_edits.pop().unwrap();
let mut actual = before;
change.edit.apply(&mut actual);
actual
};
assert_eq_text!(&after, &actual);
}
enum ExpectedResult<'a> {
NotApplicable,
After(&'a str),
Target(&'a str),
}
fn check(handler: Handler, before: &str, expected: ExpectedResult) {
let (db, file_with_caret_id, range_or_offset) = RootDatabase::with_range_or_offset(before);
let text_without_caret = db.file_text(file_with_caret_id).to_string();
let frange = FileRange { file_id: file_with_caret_id, range: range_or_offset.into() };
let sema = Semantics::new(&db);
let config = AssistConfig::default();
let ctx = AssistContext::new(sema, &config, frange);
let mut acc = Assists::new_resolved(&ctx);
handler(&mut acc, &ctx);
let mut res = acc.finish_resolved();
let assist = res.pop();
match (assist, expected) {
(Some(assist), ExpectedResult::After(after)) => {
let mut source_change = assist.source_change;
let change = source_change.source_file_edits.pop().unwrap();
let mut actual = db.file_text(change.file_id).as_ref().to_owned();
change.edit.apply(&mut actual);
assert_eq_text!(after, &actual);
}
(Some(assist), ExpectedResult::Target(target)) => {
let range = assist.assist.target;
assert_eq_text!(&text_without_caret[range], target);
}
(Some(_), ExpectedResult::NotApplicable) => panic!("assist should not be applicable!"),
(None, ExpectedResult::After(_)) | (None, ExpectedResult::Target(_)) => {
panic!("code action is not applicable")
}
(None, ExpectedResult::NotApplicable) => (),
};
}
#[test]
fn assist_order_field_struct() {
let before = "struct Foo { <|>bar: u32 }";
let (before_cursor_pos, before) = extract_offset(before);
let (db, file_id) = with_single_file(&before);
let frange = FileRange { file_id, range: TextRange::empty(before_cursor_pos) };
let assists = Assist::resolved(&db, &AssistConfig::default(), frange);
let mut assists = assists.iter();
assert_eq!(
assists.next().expect("expected assist").assist.label,
"Change visibility to pub(crate)"
);
assert_eq!(assists.next().expect("expected assist").assist.label, "Add `#[derive]`");
}
#[test]
fn assist_order_if_expr() {
let before = "
pub fn test_some_range(a: int) -> bool {
if let 2..6 = <|>5<|> {
true
} else {
false
}
}";
let (range, before) = extract_range(before);
let (db, file_id) = with_single_file(&before);
let frange = FileRange { file_id, range };
let assists = Assist::resolved(&db, &AssistConfig::default(), frange);
let mut assists = assists.iter();
assert_eq!(assists.next().expect("expected assist").assist.label, "Extract into variable");
assert_eq!(assists.next().expect("expected assist").assist.label, "Replace with match");
}
#[test]
fn assist_filter_works() {
let before = "
pub fn test_some_range(a: int) -> bool {
if let 2..6 = <|>5<|> {
true
} else {
false
}
}";
let (range, before) = extract_range(before);
let (db, file_id) = with_single_file(&before);
let frange = FileRange { file_id, range };
{
let mut cfg = AssistConfig::default();
cfg.allowed = Some(vec![AssistKind::Refactor]);
let assists = Assist::resolved(&db, &cfg, frange);
let mut assists = assists.iter();
assert_eq!(assists.next().expect("expected assist").assist.label, "Extract into variable");
assert_eq!(assists.next().expect("expected assist").assist.label, "Replace with match");
}
{
let mut cfg = AssistConfig::default();
cfg.allowed = Some(vec![AssistKind::RefactorExtract]);
let assists = Assist::resolved(&db, &cfg, frange);
assert_eq!(assists.len(), 1);
let mut assists = assists.iter();
assert_eq!(assists.next().expect("expected assist").assist.label, "Extract into variable");
}
{
let mut cfg = AssistConfig::default();
cfg.allowed = Some(vec![AssistKind::QuickFix]);
let assists = Assist::resolved(&db, &cfg, frange);
assert!(assists.is_empty(), "All asserts but quickfixes should be filtered out");
}
}

View file

@ -0,0 +1,912 @@
//! Generated file, do not edit by hand, see `xtask/src/codegen`
use super::check_doc_test;
#[test]
fn doctest_add_custom_impl() {
check_doc_test(
"add_custom_impl",
r#####"
#[derive(Deb<|>ug, Display)]
struct S;
"#####,
r#####"
#[derive(Display)]
struct S;
impl Debug for S {
$0
}
"#####,
)
}
#[test]
fn doctest_add_explicit_type() {
check_doc_test(
"add_explicit_type",
r#####"
fn main() {
let x<|> = 92;
}
"#####,
r#####"
fn main() {
let x: i32 = 92;
}
"#####,
)
}
#[test]
fn doctest_add_hash() {
check_doc_test(
"add_hash",
r#####"
fn main() {
r#"Hello,<|> World!"#;
}
"#####,
r#####"
fn main() {
r##"Hello, World!"##;
}
"#####,
)
}
#[test]
fn doctest_add_impl_default_members() {
check_doc_test(
"add_impl_default_members",
r#####"
trait Trait {
Type X;
fn foo(&self);
fn bar(&self) {}
}
impl Trait for () {
Type X = ();
fn foo(&self) {}<|>
}
"#####,
r#####"
trait Trait {
Type X;
fn foo(&self);
fn bar(&self) {}
}
impl Trait for () {
Type X = ();
fn foo(&self) {}
$0fn bar(&self) {}
}
"#####,
)
}
#[test]
fn doctest_add_impl_missing_members() {
check_doc_test(
"add_impl_missing_members",
r#####"
trait Trait<T> {
Type X;
fn foo(&self) -> T;
fn bar(&self) {}
}
impl Trait<u32> for () {<|>
}
"#####,
r#####"
trait Trait<T> {
Type X;
fn foo(&self) -> T;
fn bar(&self) {}
}
impl Trait<u32> for () {
fn foo(&self) -> u32 {
${0:todo!()}
}
}
"#####,
)
}
#[test]
fn doctest_add_turbo_fish() {
check_doc_test(
"add_turbo_fish",
r#####"
fn make<T>() -> T { todo!() }
fn main() {
let x = make<|>();
}
"#####,
r#####"
fn make<T>() -> T { todo!() }
fn main() {
let x = make::<${0:_}>();
}
"#####,
)
}
#[test]
fn doctest_apply_demorgan() {
check_doc_test(
"apply_demorgan",
r#####"
fn main() {
if x != 4 ||<|> !y {}
}
"#####,
r#####"
fn main() {
if !(x == 4 && y) {}
}
"#####,
)
}
#[test]
fn doctest_auto_import() {
check_doc_test(
"auto_import",
r#####"
fn main() {
let map = HashMap<|>::new();
}
pub mod std { pub mod collections { pub struct HashMap { } } }
"#####,
r#####"
use std::collections::HashMap;
fn main() {
let map = HashMap::new();
}
pub mod std { pub mod collections { pub struct HashMap { } } }
"#####,
)
}
#[test]
fn doctest_change_return_type_to_result() {
check_doc_test(
"change_return_type_to_result",
r#####"
fn foo() -> i32<|> { 42i32 }
"#####,
r#####"
fn foo() -> Result<i32, ${0:_}> { Ok(42i32) }
"#####,
)
}
#[test]
fn doctest_change_visibility() {
check_doc_test(
"change_visibility",
r#####"
<|>fn frobnicate() {}
"#####,
r#####"
pub(crate) fn frobnicate() {}
"#####,
)
}
#[test]
fn doctest_convert_to_guarded_return() {
check_doc_test(
"convert_to_guarded_return",
r#####"
fn main() {
<|>if cond {
foo();
bar();
}
}
"#####,
r#####"
fn main() {
if !cond {
return;
}
foo();
bar();
}
"#####,
)
}
#[test]
fn doctest_expand_glob_import() {
check_doc_test(
"expand_glob_import",
r#####"
mod foo {
pub struct Bar;
pub struct Baz;
}
use foo::*<|>;
fn qux(bar: Bar, baz: Baz) {}
"#####,
r#####"
mod foo {
pub struct Bar;
pub struct Baz;
}
use foo::{Baz, Bar};
fn qux(bar: Bar, baz: Baz) {}
"#####,
)
}
#[test]
fn doctest_extract_struct_from_enum_variant() {
check_doc_test(
"extract_struct_from_enum_variant",
r#####"
enum A { <|>One(u32, u32) }
"#####,
r#####"
struct One(pub u32, pub u32);
enum A { One(One) }
"#####,
)
}
#[test]
fn doctest_extract_variable() {
check_doc_test(
"extract_variable",
r#####"
fn main() {
<|>(1 + 2)<|> * 4;
}
"#####,
r#####"
fn main() {
let $0var_name = (1 + 2);
var_name * 4;
}
"#####,
)
}
#[test]
fn doctest_fill_match_arms() {
check_doc_test(
"fill_match_arms",
r#####"
enum Action { Move { distance: u32 }, Stop }
fn handle(action: Action) {
match action {
<|>
}
}
"#####,
r#####"
enum Action { Move { distance: u32 }, Stop }
fn handle(action: Action) {
match action {
$0Action::Move { distance } => {}
Action::Stop => {}
}
}
"#####,
)
}
#[test]
fn doctest_fix_visibility() {
check_doc_test(
"fix_visibility",
r#####"
mod m {
fn frobnicate() {}
}
fn main() {
m::frobnicate<|>() {}
}
"#####,
r#####"
mod m {
$0pub(crate) fn frobnicate() {}
}
fn main() {
m::frobnicate() {}
}
"#####,
)
}
#[test]
fn doctest_flip_binexpr() {
check_doc_test(
"flip_binexpr",
r#####"
fn main() {
let _ = 90 +<|> 2;
}
"#####,
r#####"
fn main() {
let _ = 2 + 90;
}
"#####,
)
}
#[test]
fn doctest_flip_comma() {
check_doc_test(
"flip_comma",
r#####"
fn main() {
((1, 2),<|> (3, 4));
}
"#####,
r#####"
fn main() {
((3, 4), (1, 2));
}
"#####,
)
}
#[test]
fn doctest_flip_trait_bound() {
check_doc_test(
"flip_trait_bound",
r#####"
fn foo<T: Clone +<|> Copy>() { }
"#####,
r#####"
fn foo<T: Copy + Clone>() { }
"#####,
)
}
#[test]
fn doctest_generate_derive() {
check_doc_test(
"generate_derive",
r#####"
struct Point {
x: u32,
y: u32,<|>
}
"#####,
r#####"
#[derive($0)]
struct Point {
x: u32,
y: u32,
}
"#####,
)
}
#[test]
fn doctest_generate_from_impl_for_enum() {
check_doc_test(
"generate_from_impl_for_enum",
r#####"
enum A { <|>One(u32) }
"#####,
r#####"
enum A { One(u32) }
impl From<u32> for A {
fn from(v: u32) -> Self {
A::One(v)
}
}
"#####,
)
}
#[test]
fn doctest_generate_function() {
check_doc_test(
"generate_function",
r#####"
struct Baz;
fn baz() -> Baz { Baz }
fn foo() {
bar<|>("", baz());
}
"#####,
r#####"
struct Baz;
fn baz() -> Baz { Baz }
fn foo() {
bar("", baz());
}
fn bar(arg: &str, baz: Baz) {
${0:todo!()}
}
"#####,
)
}
#[test]
fn doctest_generate_impl() {
check_doc_test(
"generate_impl",
r#####"
struct Ctx<T: Clone> {
data: T,<|>
}
"#####,
r#####"
struct Ctx<T: Clone> {
data: T,
}
impl<T: Clone> Ctx<T> {
$0
}
"#####,
)
}
#[test]
fn doctest_generate_new() {
check_doc_test(
"generate_new",
r#####"
struct Ctx<T: Clone> {
data: T,<|>
}
"#####,
r#####"
struct Ctx<T: Clone> {
data: T,
}
impl<T: Clone> Ctx<T> {
fn $0new(data: T) -> Self { Self { data } }
}
"#####,
)
}
#[test]
fn doctest_inline_local_variable() {
check_doc_test(
"inline_local_variable",
r#####"
fn main() {
let x<|> = 1 + 2;
x * 4;
}
"#####,
r#####"
fn main() {
(1 + 2) * 4;
}
"#####,
)
}
#[test]
fn doctest_introduce_named_lifetime() {
check_doc_test(
"introduce_named_lifetime",
r#####"
impl Cursor<'_<|>> {
fn node(self) -> &SyntaxNode {
match self {
Cursor::Replace(node) | Cursor::Before(node) => node,
}
}
}
"#####,
r#####"
impl<'a> Cursor<'a> {
fn node(self) -> &SyntaxNode {
match self {
Cursor::Replace(node) | Cursor::Before(node) => node,
}
}
}
"#####,
)
}
#[test]
fn doctest_invert_if() {
check_doc_test(
"invert_if",
r#####"
fn main() {
if<|> !y { A } else { B }
}
"#####,
r#####"
fn main() {
if y { B } else { A }
}
"#####,
)
}
#[test]
fn doctest_make_raw_string() {
check_doc_test(
"make_raw_string",
r#####"
fn main() {
"Hello,<|> World!";
}
"#####,
r#####"
fn main() {
r#"Hello, World!"#;
}
"#####,
)
}
#[test]
fn doctest_make_usual_string() {
check_doc_test(
"make_usual_string",
r#####"
fn main() {
r#"Hello,<|> "World!""#;
}
"#####,
r#####"
fn main() {
"Hello, \"World!\"";
}
"#####,
)
}
#[test]
fn doctest_merge_imports() {
check_doc_test(
"merge_imports",
r#####"
use std::<|>fmt::Formatter;
use std::io;
"#####,
r#####"
use std::{fmt::Formatter, io};
"#####,
)
}
#[test]
fn doctest_merge_match_arms() {
check_doc_test(
"merge_match_arms",
r#####"
enum Action { Move { distance: u32 }, Stop }
fn handle(action: Action) {
match action {
<|>Action::Move(..) => foo(),
Action::Stop => foo(),
}
}
"#####,
r#####"
enum Action { Move { distance: u32 }, Stop }
fn handle(action: Action) {
match action {
Action::Move(..) | Action::Stop => foo(),
}
}
"#####,
)
}
#[test]
fn doctest_move_arm_cond_to_match_guard() {
check_doc_test(
"move_arm_cond_to_match_guard",
r#####"
enum Action { Move { distance: u32 }, Stop }
fn handle(action: Action) {
match action {
Action::Move { distance } => <|>if distance > 10 { foo() },
_ => (),
}
}
"#####,
r#####"
enum Action { Move { distance: u32 }, Stop }
fn handle(action: Action) {
match action {
Action::Move { distance } if distance > 10 => foo(),
_ => (),
}
}
"#####,
)
}
#[test]
fn doctest_move_bounds_to_where_clause() {
check_doc_test(
"move_bounds_to_where_clause",
r#####"
fn apply<T, U, <|>F: FnOnce(T) -> U>(f: F, x: T) -> U {
f(x)
}
"#####,
r#####"
fn apply<T, U, F>(f: F, x: T) -> U where F: FnOnce(T) -> U {
f(x)
}
"#####,
)
}
#[test]
fn doctest_move_guard_to_arm_body() {
check_doc_test(
"move_guard_to_arm_body",
r#####"
enum Action { Move { distance: u32 }, Stop }
fn handle(action: Action) {
match action {
Action::Move { distance } <|>if distance > 10 => foo(),
_ => (),
}
}
"#####,
r#####"
enum Action { Move { distance: u32 }, Stop }
fn handle(action: Action) {
match action {
Action::Move { distance } => if distance > 10 {
foo()
},
_ => (),
}
}
"#####,
)
}
#[test]
fn doctest_remove_dbg() {
check_doc_test(
"remove_dbg",
r#####"
fn main() {
<|>dbg!(92);
}
"#####,
r#####"
fn main() {
92;
}
"#####,
)
}
#[test]
fn doctest_remove_hash() {
check_doc_test(
"remove_hash",
r#####"
fn main() {
r#"Hello,<|> World!"#;
}
"#####,
r#####"
fn main() {
r"Hello, World!";
}
"#####,
)
}
#[test]
fn doctest_remove_mut() {
check_doc_test(
"remove_mut",
r#####"
impl Walrus {
fn feed(&mut<|> self, amount: u32) {}
}
"#####,
r#####"
impl Walrus {
fn feed(&self, amount: u32) {}
}
"#####,
)
}
#[test]
fn doctest_remove_unused_param() {
check_doc_test(
"remove_unused_param",
r#####"
fn frobnicate(x: i32<|>) {}
fn main() {
frobnicate(92);
}
"#####,
r#####"
fn frobnicate() {}
fn main() {
frobnicate();
}
"#####,
)
}
#[test]
fn doctest_reorder_fields() {
check_doc_test(
"reorder_fields",
r#####"
struct Foo {foo: i32, bar: i32};
const test: Foo = <|>Foo {bar: 0, foo: 1}
"#####,
r#####"
struct Foo {foo: i32, bar: i32};
const test: Foo = Foo {foo: 1, bar: 0}
"#####,
)
}
#[test]
fn doctest_replace_if_let_with_match() {
check_doc_test(
"replace_if_let_with_match",
r#####"
enum Action { Move { distance: u32 }, Stop }
fn handle(action: Action) {
<|>if let Action::Move { distance } = action {
foo(distance)
} else {
bar()
}
}
"#####,
r#####"
enum Action { Move { distance: u32 }, Stop }
fn handle(action: Action) {
match action {
Action::Move { distance } => foo(distance),
_ => bar(),
}
}
"#####,
)
}
#[test]
fn doctest_replace_let_with_if_let() {
check_doc_test(
"replace_let_with_if_let",
r#####"
enum Option<T> { Some(T), None }
fn main(action: Action) {
<|>let x = compute();
}
fn compute() -> Option<i32> { None }
"#####,
r#####"
enum Option<T> { Some(T), None }
fn main(action: Action) {
if let Some(x) = compute() {
}
}
fn compute() -> Option<i32> { None }
"#####,
)
}
#[test]
fn doctest_replace_qualified_name_with_use() {
check_doc_test(
"replace_qualified_name_with_use",
r#####"
fn process(map: std::collections::<|>HashMap<String, String>) {}
"#####,
r#####"
use std::collections::HashMap;
fn process(map: HashMap<String, String>) {}
"#####,
)
}
#[test]
fn doctest_replace_unwrap_with_match() {
check_doc_test(
"replace_unwrap_with_match",
r#####"
enum Result<T, E> { Ok(T), Err(E) }
fn main() {
let x: Result<i32, i32> = Result::Ok(92);
let y = x.<|>unwrap();
}
"#####,
r#####"
enum Result<T, E> { Ok(T), Err(E) }
fn main() {
let x: Result<i32, i32> = Result::Ok(92);
let y = match x {
Ok(a) => a,
$0_ => unreachable!(),
};
}
"#####,
)
}
#[test]
fn doctest_split_import() {
check_doc_test(
"split_import",
r#####"
use std::<|>collections::HashMap;
"#####,
r#####"
use std::{collections::HashMap};
"#####,
)
}
#[test]
fn doctest_unwrap_block() {
check_doc_test(
"unwrap_block",
r#####"
fn foo() {
if true {<|>
println!("foo");
}
}
"#####,
r#####"
fn foo() {
println!("foo");
}
"#####,
)
}

317
crates/assists/src/utils.rs Normal file
View file

@ -0,0 +1,317 @@
//! Assorted functions shared by several assists.
pub(crate) mod insert_use;
use std::{iter, ops};
use hir::{Adt, Crate, Enum, ScopeDef, Semantics, Trait, Type};
use ide_db::RootDatabase;
use itertools::Itertools;
use rustc_hash::FxHashSet;
use syntax::{
ast::{self, make, NameOwner},
AstNode, Direction,
SyntaxKind::*,
SyntaxNode, TextSize, T,
};
use crate::assist_config::SnippetCap;
pub(crate) use insert_use::{find_insert_use_container, insert_use_statement};
pub(crate) fn unwrap_trivial_block(block: ast::BlockExpr) -> ast::Expr {
extract_trivial_expression(&block)
.filter(|expr| !expr.syntax().text().contains_char('\n'))
.unwrap_or_else(|| block.into())
}
pub fn extract_trivial_expression(block: &ast::BlockExpr) -> Option<ast::Expr> {
let has_anything_else = |thing: &SyntaxNode| -> bool {
let mut non_trivial_children =
block.syntax().children_with_tokens().filter(|it| match it.kind() {
WHITESPACE | T!['{'] | T!['}'] => false,
_ => it.as_node() != Some(thing),
});
non_trivial_children.next().is_some()
};
if let Some(expr) = block.expr() {
if has_anything_else(expr.syntax()) {
return None;
}
return Some(expr);
}
// Unwrap `{ continue; }`
let (stmt,) = block.statements().next_tuple()?;
if let ast::Stmt::ExprStmt(expr_stmt) = stmt {
if has_anything_else(expr_stmt.syntax()) {
return None;
}
let expr = expr_stmt.expr()?;
match expr.syntax().kind() {
CONTINUE_EXPR | BREAK_EXPR | RETURN_EXPR => return Some(expr),
_ => (),
}
}
None
}
#[derive(Clone, Copy, Debug)]
pub(crate) enum Cursor<'a> {
Replace(&'a SyntaxNode),
Before(&'a SyntaxNode),
}
impl<'a> Cursor<'a> {
fn node(self) -> &'a SyntaxNode {
match self {
Cursor::Replace(node) | Cursor::Before(node) => node,
}
}
}
pub(crate) fn render_snippet(_cap: SnippetCap, node: &SyntaxNode, cursor: Cursor) -> String {
assert!(cursor.node().ancestors().any(|it| it == *node));
let range = cursor.node().text_range() - node.text_range().start();
let range: ops::Range<usize> = range.into();
let mut placeholder = cursor.node().to_string();
escape(&mut placeholder);
let tab_stop = match cursor {
Cursor::Replace(placeholder) => format!("${{0:{}}}", placeholder),
Cursor::Before(placeholder) => format!("$0{}", placeholder),
};
let mut buf = node.to_string();
buf.replace_range(range, &tab_stop);
return buf;
fn escape(buf: &mut String) {
stdx::replace(buf, '{', r"\{");
stdx::replace(buf, '}', r"\}");
stdx::replace(buf, '$', r"\$");
}
}
pub fn get_missing_assoc_items(
sema: &Semantics<RootDatabase>,
impl_def: &ast::Impl,
) -> Vec<hir::AssocItem> {
// Names must be unique between constants and functions. However, type aliases
// may share the same name as a function or constant.
let mut impl_fns_consts = FxHashSet::default();
let mut impl_type = FxHashSet::default();
if let Some(item_list) = impl_def.assoc_item_list() {
for item in item_list.assoc_items() {
match item {
ast::AssocItem::Fn(f) => {
if let Some(n) = f.name() {
impl_fns_consts.insert(n.syntax().to_string());
}
}
ast::AssocItem::TypeAlias(t) => {
if let Some(n) = t.name() {
impl_type.insert(n.syntax().to_string());
}
}
ast::AssocItem::Const(c) => {
if let Some(n) = c.name() {
impl_fns_consts.insert(n.syntax().to_string());
}
}
ast::AssocItem::MacroCall(_) => (),
}
}
}
resolve_target_trait(sema, impl_def).map_or(vec![], |target_trait| {
target_trait
.items(sema.db)
.iter()
.filter(|i| match i {
hir::AssocItem::Function(f) => {
!impl_fns_consts.contains(&f.name(sema.db).to_string())
}
hir::AssocItem::TypeAlias(t) => !impl_type.contains(&t.name(sema.db).to_string()),
hir::AssocItem::Const(c) => c
.name(sema.db)
.map(|n| !impl_fns_consts.contains(&n.to_string()))
.unwrap_or_default(),
})
.cloned()
.collect()
})
}
pub(crate) fn resolve_target_trait(
sema: &Semantics<RootDatabase>,
impl_def: &ast::Impl,
) -> Option<hir::Trait> {
let ast_path =
impl_def.trait_().map(|it| it.syntax().clone()).and_then(ast::PathType::cast)?.path()?;
match sema.resolve_path(&ast_path) {
Some(hir::PathResolution::Def(hir::ModuleDef::Trait(def))) => Some(def),
_ => None,
}
}
pub(crate) fn vis_offset(node: &SyntaxNode) -> TextSize {
node.children_with_tokens()
.find(|it| !matches!(it.kind(), WHITESPACE | COMMENT | ATTR))
.map(|it| it.text_range().start())
.unwrap_or_else(|| node.text_range().start())
}
pub(crate) fn invert_boolean_expression(expr: ast::Expr) -> ast::Expr {
if let Some(expr) = invert_special_case(&expr) {
return expr;
}
make::expr_prefix(T![!], expr)
}
fn invert_special_case(expr: &ast::Expr) -> Option<ast::Expr> {
match expr {
ast::Expr::BinExpr(bin) => match bin.op_kind()? {
ast::BinOp::NegatedEqualityTest => bin.replace_op(T![==]).map(|it| it.into()),
ast::BinOp::EqualityTest => bin.replace_op(T![!=]).map(|it| it.into()),
_ => None,
},
ast::Expr::PrefixExpr(pe) if pe.op_kind()? == ast::PrefixOp::Not => pe.expr(),
// FIXME:
// ast::Expr::Literal(true | false )
_ => None,
}
}
#[derive(Clone, Copy)]
pub enum TryEnum {
Result,
Option,
}
impl TryEnum {
const ALL: [TryEnum; 2] = [TryEnum::Option, TryEnum::Result];
pub fn from_ty(sema: &Semantics<RootDatabase>, ty: &Type) -> Option<TryEnum> {
let enum_ = match ty.as_adt() {
Some(Adt::Enum(it)) => it,
_ => return None,
};
TryEnum::ALL.iter().find_map(|&var| {
if &enum_.name(sema.db).to_string() == var.type_name() {
return Some(var);
}
None
})
}
pub(crate) fn happy_case(self) -> &'static str {
match self {
TryEnum::Result => "Ok",
TryEnum::Option => "Some",
}
}
pub(crate) fn sad_pattern(self) -> ast::Pat {
match self {
TryEnum::Result => make::tuple_struct_pat(
make::path_unqualified(make::path_segment(make::name_ref("Err"))),
iter::once(make::wildcard_pat().into()),
)
.into(),
TryEnum::Option => make::ident_pat(make::name("None")).into(),
}
}
fn type_name(self) -> &'static str {
match self {
TryEnum::Result => "Result",
TryEnum::Option => "Option",
}
}
}
/// Helps with finding well-know things inside the standard library. This is
/// somewhat similar to the known paths infra inside hir, but it different; We
/// want to make sure that IDE specific paths don't become interesting inside
/// the compiler itself as well.
pub(crate) struct FamousDefs<'a, 'b>(pub(crate) &'a Semantics<'b, RootDatabase>, pub(crate) Crate);
#[allow(non_snake_case)]
impl FamousDefs<'_, '_> {
#[cfg(test)]
pub(crate) const FIXTURE: &'static str = r#"//- /libcore.rs crate:core
pub mod convert {
pub trait From<T> {
fn from(T) -> Self;
}
}
pub mod option {
pub enum Option<T> { None, Some(T)}
}
pub mod prelude {
pub use crate::{convert::From, option::Option::{self, *}};
}
#[prelude_import]
pub use prelude::*;
"#;
pub(crate) fn core_convert_From(&self) -> Option<Trait> {
self.find_trait("core:convert:From")
}
pub(crate) fn core_option_Option(&self) -> Option<Enum> {
self.find_enum("core:option:Option")
}
fn find_trait(&self, path: &str) -> Option<Trait> {
match self.find_def(path)? {
hir::ScopeDef::ModuleDef(hir::ModuleDef::Trait(it)) => Some(it),
_ => None,
}
}
fn find_enum(&self, path: &str) -> Option<Enum> {
match self.find_def(path)? {
hir::ScopeDef::ModuleDef(hir::ModuleDef::Adt(hir::Adt::Enum(it))) => Some(it),
_ => None,
}
}
fn find_def(&self, path: &str) -> Option<ScopeDef> {
let db = self.0.db;
let mut path = path.split(':');
let trait_ = path.next_back()?;
let std_crate = path.next()?;
let std_crate = self
.1
.dependencies(db)
.into_iter()
.find(|dep| &dep.name.to_string() == std_crate)?
.krate;
let mut module = std_crate.root_module(db);
for segment in path {
module = module.children(db).find_map(|child| {
let name = child.name(db)?;
if &name.to_string() == segment {
Some(child)
} else {
None
}
})?;
}
let def =
module.scope(db, None).into_iter().find(|(name, _def)| &name.to_string() == trait_)?.1;
Some(def)
}
}
pub(crate) fn next_prev() -> impl Iterator<Item = Direction> {
[Direction::Next, Direction::Prev].iter().copied()
}

View file

@ -0,0 +1,546 @@
//! Handle syntactic aspects of inserting a new `use`.
// FIXME: rewrite according to the plan, outlined in
// https://github.com/rust-analyzer/rust-analyzer/issues/3301#issuecomment-592931553
use std::iter::successors;
use either::Either;
use syntax::{
ast::{self, NameOwner, VisibilityOwner},
AstNode, AstToken, Direction, SmolStr,
SyntaxKind::{PATH, PATH_SEGMENT},
SyntaxNode, SyntaxToken, T,
};
use text_edit::TextEditBuilder;
use crate::assist_context::AssistContext;
/// Determines the containing syntax node in which to insert a `use` statement affecting `position`.
pub(crate) fn find_insert_use_container(
position: &SyntaxNode,
ctx: &AssistContext,
) -> Option<Either<ast::ItemList, ast::SourceFile>> {
ctx.sema.ancestors_with_macros(position.clone()).find_map(|n| {
if let Some(module) = ast::Module::cast(n.clone()) {
return module.item_list().map(|it| Either::Left(it));
}
Some(Either::Right(ast::SourceFile::cast(n)?))
})
}
/// Creates and inserts a use statement for the given path to import.
/// The use statement is inserted in the scope most appropriate to the
/// the cursor position given, additionally merged with the existing use imports.
pub(crate) fn insert_use_statement(
// Ideally the position of the cursor, used to
position: &SyntaxNode,
path_to_import: &str,
ctx: &AssistContext,
builder: &mut TextEditBuilder,
) {
let target = path_to_import.split("::").map(SmolStr::new).collect::<Vec<_>>();
let container = find_insert_use_container(position, ctx);
if let Some(container) = container {
let syntax = container.either(|l| l.syntax().clone(), |r| r.syntax().clone());
let action = best_action_for_target(syntax, position.clone(), &target);
make_assist(&action, &target, builder);
}
}
fn collect_path_segments_raw(
segments: &mut Vec<ast::PathSegment>,
mut path: ast::Path,
) -> Option<usize> {
let oldlen = segments.len();
loop {
let mut children = path.syntax().children_with_tokens();
let (first, second, third) = (
children.next().map(|n| (n.clone(), n.kind())),
children.next().map(|n| (n.clone(), n.kind())),
children.next().map(|n| (n.clone(), n.kind())),
);
match (first, second, third) {
(Some((subpath, PATH)), Some((_, T![::])), Some((segment, PATH_SEGMENT))) => {
path = ast::Path::cast(subpath.as_node()?.clone())?;
segments.push(ast::PathSegment::cast(segment.as_node()?.clone())?);
}
(Some((segment, PATH_SEGMENT)), _, _) => {
segments.push(ast::PathSegment::cast(segment.as_node()?.clone())?);
break;
}
(_, _, _) => return None,
}
}
// We need to reverse only the new added segments
let only_new_segments = segments.split_at_mut(oldlen).1;
only_new_segments.reverse();
Some(segments.len() - oldlen)
}
fn fmt_segments_raw(segments: &[SmolStr], buf: &mut String) {
let mut iter = segments.iter();
if let Some(s) = iter.next() {
buf.push_str(s);
}
for s in iter {
buf.push_str("::");
buf.push_str(s);
}
}
/// Returns the number of common segments.
fn compare_path_segments(left: &[SmolStr], right: &[ast::PathSegment]) -> usize {
left.iter().zip(right).take_while(|(l, r)| compare_path_segment(l, r)).count()
}
fn compare_path_segment(a: &SmolStr, b: &ast::PathSegment) -> bool {
if let Some(kb) = b.kind() {
match kb {
ast::PathSegmentKind::Name(nameref_b) => a == nameref_b.text(),
ast::PathSegmentKind::SelfKw => a == "self",
ast::PathSegmentKind::SuperKw => a == "super",
ast::PathSegmentKind::CrateKw => a == "crate",
ast::PathSegmentKind::Type { .. } => false, // not allowed in imports
}
} else {
false
}
}
fn compare_path_segment_with_name(a: &SmolStr, b: &ast::Name) -> bool {
a == b.text()
}
#[derive(Clone, Debug)]
enum ImportAction {
Nothing,
// Add a brand new use statement.
AddNewUse {
anchor: Option<SyntaxNode>, // anchor node
add_after_anchor: bool,
},
// To split an existing use statement creating a nested import.
AddNestedImport {
// how may segments matched with the target path
common_segments: usize,
path_to_split: ast::Path,
// the first segment of path_to_split we want to add into the new nested list
first_segment_to_split: Option<ast::PathSegment>,
// Wether to add 'self' in addition to the target path
add_self: bool,
},
// To add the target path to an existing nested import tree list.
AddInTreeList {
common_segments: usize,
// The UseTreeList where to add the target path
tree_list: ast::UseTreeList,
add_self: bool,
},
}
impl ImportAction {
fn add_new_use(anchor: Option<SyntaxNode>, add_after_anchor: bool) -> Self {
ImportAction::AddNewUse { anchor, add_after_anchor }
}
fn add_nested_import(
common_segments: usize,
path_to_split: ast::Path,
first_segment_to_split: Option<ast::PathSegment>,
add_self: bool,
) -> Self {
ImportAction::AddNestedImport {
common_segments,
path_to_split,
first_segment_to_split,
add_self,
}
}
fn add_in_tree_list(
common_segments: usize,
tree_list: ast::UseTreeList,
add_self: bool,
) -> Self {
ImportAction::AddInTreeList { common_segments, tree_list, add_self }
}
fn better(left: ImportAction, right: ImportAction) -> ImportAction {
if left.is_better(&right) {
left
} else {
right
}
}
fn is_better(&self, other: &ImportAction) -> bool {
match (self, other) {
(ImportAction::Nothing, _) => true,
(ImportAction::AddInTreeList { .. }, ImportAction::Nothing) => false,
(
ImportAction::AddNestedImport { common_segments: n, .. },
ImportAction::AddInTreeList { common_segments: m, .. },
)
| (
ImportAction::AddInTreeList { common_segments: n, .. },
ImportAction::AddNestedImport { common_segments: m, .. },
)
| (
ImportAction::AddInTreeList { common_segments: n, .. },
ImportAction::AddInTreeList { common_segments: m, .. },
)
| (
ImportAction::AddNestedImport { common_segments: n, .. },
ImportAction::AddNestedImport { common_segments: m, .. },
) => n > m,
(ImportAction::AddInTreeList { .. }, _) => true,
(ImportAction::AddNestedImport { .. }, ImportAction::Nothing) => false,
(ImportAction::AddNestedImport { .. }, _) => true,
(ImportAction::AddNewUse { .. }, _) => false,
}
}
}
// Find out the best ImportAction to import target path against current_use_tree.
// If current_use_tree has a nested import the function gets called recursively on every UseTree inside a UseTreeList.
fn walk_use_tree_for_best_action(
current_path_segments: &mut Vec<ast::PathSegment>, // buffer containing path segments
current_parent_use_tree_list: Option<ast::UseTreeList>, // will be Some value if we are in a nested import
current_use_tree: ast::UseTree, // the use tree we are currently examinating
target: &[SmolStr], // the path we want to import
) -> ImportAction {
// We save the number of segments in the buffer so we can restore the correct segments
// before returning. Recursive call will add segments so we need to delete them.
let prev_len = current_path_segments.len();
let tree_list = current_use_tree.use_tree_list();
let alias = current_use_tree.rename();
let path = match current_use_tree.path() {
Some(path) => path,
None => {
// If the use item don't have a path, it means it's broken (syntax error)
return ImportAction::add_new_use(
current_use_tree
.syntax()
.ancestors()
.find_map(ast::Use::cast)
.map(|it| it.syntax().clone()),
true,
);
}
};
// This can happen only if current_use_tree is a direct child of a UseItem
if let Some(name) = alias.and_then(|it| it.name()) {
if compare_path_segment_with_name(&target[0], &name) {
return ImportAction::Nothing;
}
}
collect_path_segments_raw(current_path_segments, path.clone());
// We compare only the new segments added in the line just above.
// The first prev_len segments were already compared in 'parent' recursive calls.
let left = target.split_at(prev_len).1;
let right = current_path_segments.split_at(prev_len).1;
let common = compare_path_segments(left, &right);
let mut action = match common {
0 => ImportAction::add_new_use(
// e.g: target is std::fmt and we can have
// use foo::bar
// We add a brand new use statement
current_use_tree
.syntax()
.ancestors()
.find_map(ast::Use::cast)
.map(|it| it.syntax().clone()),
true,
),
common if common == left.len() && left.len() == right.len() => {
// e.g: target is std::fmt and we can have
// 1- use std::fmt;
// 2- use std::fmt::{ ... }
if let Some(list) = tree_list {
// In case 2 we need to add self to the nested list
// unless it's already there
let has_self = list.use_trees().map(|it| it.path()).any(|p| {
p.and_then(|it| it.segment())
.and_then(|it| it.kind())
.filter(|k| *k == ast::PathSegmentKind::SelfKw)
.is_some()
});
if has_self {
ImportAction::Nothing
} else {
ImportAction::add_in_tree_list(current_path_segments.len(), list, true)
}
} else {
// Case 1
ImportAction::Nothing
}
}
common if common != left.len() && left.len() == right.len() => {
// e.g: target is std::fmt and we have
// use std::io;
// We need to split.
let segments_to_split = current_path_segments.split_at(prev_len + common).1;
ImportAction::add_nested_import(
prev_len + common,
path,
Some(segments_to_split[0].clone()),
false,
)
}
common if common == right.len() && left.len() > right.len() => {
// e.g: target is std::fmt and we can have
// 1- use std;
// 2- use std::{ ... };
// fallback action
let mut better_action = ImportAction::add_new_use(
current_use_tree
.syntax()
.ancestors()
.find_map(ast::Use::cast)
.map(|it| it.syntax().clone()),
true,
);
if let Some(list) = tree_list {
// Case 2, check recursively if the path is already imported in the nested list
for u in list.use_trees() {
let child_action = walk_use_tree_for_best_action(
current_path_segments,
Some(list.clone()),
u,
target,
);
if child_action.is_better(&better_action) {
better_action = child_action;
if let ImportAction::Nothing = better_action {
return better_action;
}
}
}
} else {
// Case 1, split adding self
better_action = ImportAction::add_nested_import(prev_len + common, path, None, true)
}
better_action
}
common if common == left.len() && left.len() < right.len() => {
// e.g: target is std::fmt and we can have
// use std::fmt::Debug;
let segments_to_split = current_path_segments.split_at(prev_len + common).1;
ImportAction::add_nested_import(
prev_len + common,
path,
Some(segments_to_split[0].clone()),
true,
)
}
common if common < left.len() && common < right.len() => {
// e.g: target is std::fmt::nested::Debug
// use std::fmt::Display
let segments_to_split = current_path_segments.split_at(prev_len + common).1;
ImportAction::add_nested_import(
prev_len + common,
path,
Some(segments_to_split[0].clone()),
false,
)
}
_ => unreachable!(),
};
// If we are inside a UseTreeList adding a use statement become adding to the existing
// tree list.
action = match (current_parent_use_tree_list, action.clone()) {
(Some(use_tree_list), ImportAction::AddNewUse { .. }) => {
ImportAction::add_in_tree_list(prev_len, use_tree_list, false)
}
(_, _) => action,
};
// We remove the segments added
current_path_segments.truncate(prev_len);
action
}
fn best_action_for_target(
container: SyntaxNode,
anchor: SyntaxNode,
target: &[SmolStr],
) -> ImportAction {
let mut storage = Vec::with_capacity(16); // this should be the only allocation
let best_action = container
.children()
.filter_map(ast::Use::cast)
.filter(|u| u.visibility().is_none())
.filter_map(|it| it.use_tree())
.map(|u| walk_use_tree_for_best_action(&mut storage, None, u, target))
.fold(None, |best, a| match best {
Some(best) => Some(ImportAction::better(best, a)),
None => Some(a),
});
match best_action {
Some(action) => action,
None => {
// We have no action and no UseItem was found in container so we find
// another item and we use it as anchor.
// If there are no items above, we choose the target path itself as anchor.
// todo: we should include even whitespace blocks as anchor candidates
let anchor = container.children().next().or_else(|| Some(anchor));
let add_after_anchor = anchor
.clone()
.and_then(ast::Attr::cast)
.map(|attr| attr.kind() == ast::AttrKind::Inner)
.unwrap_or(false);
ImportAction::add_new_use(anchor, add_after_anchor)
}
}
}
fn make_assist(action: &ImportAction, target: &[SmolStr], edit: &mut TextEditBuilder) {
match action {
ImportAction::AddNewUse { anchor, add_after_anchor } => {
make_assist_add_new_use(anchor, *add_after_anchor, target, edit)
}
ImportAction::AddInTreeList { common_segments, tree_list, add_self } => {
// We know that the fist n segments already exists in the use statement we want
// to modify, so we want to add only the last target.len() - n segments.
let segments_to_add = target.split_at(*common_segments).1;
make_assist_add_in_tree_list(tree_list, segments_to_add, *add_self, edit)
}
ImportAction::AddNestedImport {
common_segments,
path_to_split,
first_segment_to_split,
add_self,
} => {
let segments_to_add = target.split_at(*common_segments).1;
make_assist_add_nested_import(
path_to_split,
first_segment_to_split,
segments_to_add,
*add_self,
edit,
)
}
_ => {}
}
}
fn make_assist_add_new_use(
anchor: &Option<SyntaxNode>,
after: bool,
target: &[SmolStr],
edit: &mut TextEditBuilder,
) {
if let Some(anchor) = anchor {
let indent = leading_indent(anchor);
let mut buf = String::new();
if after {
buf.push_str("\n");
if let Some(spaces) = &indent {
buf.push_str(spaces);
}
}
buf.push_str("use ");
fmt_segments_raw(target, &mut buf);
buf.push_str(";");
if !after {
buf.push_str("\n\n");
if let Some(spaces) = &indent {
buf.push_str(&spaces);
}
}
let position = if after { anchor.text_range().end() } else { anchor.text_range().start() };
edit.insert(position, buf);
}
}
fn make_assist_add_in_tree_list(
tree_list: &ast::UseTreeList,
target: &[SmolStr],
add_self: bool,
edit: &mut TextEditBuilder,
) {
let last = tree_list.use_trees().last();
if let Some(last) = last {
let mut buf = String::new();
let comma = last.syntax().siblings(Direction::Next).find(|n| n.kind() == T![,]);
let offset = if let Some(comma) = comma {
comma.text_range().end()
} else {
buf.push_str(",");
last.syntax().text_range().end()
};
if add_self {
buf.push_str(" self")
} else {
buf.push_str(" ");
}
fmt_segments_raw(target, &mut buf);
edit.insert(offset, buf);
} else {
}
}
fn make_assist_add_nested_import(
path: &ast::Path,
first_segment_to_split: &Option<ast::PathSegment>,
target: &[SmolStr],
add_self: bool,
edit: &mut TextEditBuilder,
) {
let use_tree = path.syntax().ancestors().find_map(ast::UseTree::cast);
if let Some(use_tree) = use_tree {
let (start, add_colon_colon) = if let Some(first_segment_to_split) = first_segment_to_split
{
(first_segment_to_split.syntax().text_range().start(), false)
} else {
(use_tree.syntax().text_range().end(), true)
};
let end = use_tree.syntax().text_range().end();
let mut buf = String::new();
if add_colon_colon {
buf.push_str("::");
}
buf.push_str("{");
if add_self {
buf.push_str("self, ");
}
fmt_segments_raw(target, &mut buf);
if !target.is_empty() {
buf.push_str(", ");
}
edit.insert(start, buf);
edit.insert(end, "}".to_string());
}
}
/// If the node is on the beginning of the line, calculate indent.
fn leading_indent(node: &SyntaxNode) -> Option<SmolStr> {
for token in prev_tokens(node.first_token()?) {
if let Some(ws) = ast::Whitespace::cast(token.clone()) {
let ws_text = ws.text();
if let Some(pos) = ws_text.rfind('\n') {
return Some(ws_text[pos + 1..].into());
}
}
if token.text().contains('\n') {
break;
}
}
return None;
fn prev_tokens(token: SyntaxToken) -> impl Iterator<Item = SyntaxToken> {
successors(token.prev_token(), |token| token.prev_token())
}
}

21
crates/base_db/Cargo.toml Normal file
View file

@ -0,0 +1,21 @@
[package]
name = "base_db"
version = "0.0.0"
license = "MIT OR Apache-2.0"
authors = ["rust-analyzer developers"]
edition = "2018"
[lib]
doctest = false
[dependencies]
salsa = "0.15.2"
rustc-hash = "1.1.0"
syntax = { path = "../syntax" }
cfg = { path = "../cfg" }
profile = { path = "../profile" }
tt = { path = "../tt" }
test_utils = { path = "../test_utils" }
vfs = { path = "../vfs" }
stdx = { path = "../stdx" }

View file

@ -0,0 +1,228 @@
//! Fixtures are strings containing rust source code with optional metadata.
//! A fixture without metadata is parsed into a single source file.
//! Use this to test functionality local to one file.
//!
//! Simple Example:
//! ```
//! r#"
//! fn main() {
//! println!("Hello World")
//! }
//! "#
//! ```
//!
//! Metadata can be added to a fixture after a `//-` comment.
//! The basic form is specifying filenames,
//! which is also how to define multiple files in a single test fixture
//!
//! Example using two files in the same crate:
//! ```
//! "
//! //- /main.rs
//! mod foo;
//! fn main() {
//! foo::bar();
//! }
//!
//! //- /foo.rs
//! pub fn bar() {}
//! "
//! ```
//!
//! Example using two crates with one file each, with one crate depending on the other:
//! ```
//! r#"
//! //- /main.rs crate:a deps:b
//! fn main() {
//! b::foo();
//! }
//! //- /lib.rs crate:b
//! pub fn b() {
//! println!("Hello World")
//! }
//! "#
//! ```
//!
//! Metadata allows specifying all settings and variables
//! that are available in a real rust project:
//! - crate names via `crate:cratename`
//! - dependencies via `deps:dep1,dep2`
//! - configuration settings via `cfg:dbg=false,opt_level=2`
//! - environment variables via `env:PATH=/bin,RUST_LOG=debug`
//!
//! Example using all available metadata:
//! ```
//! "
//! //- /lib.rs crate:foo deps:bar,baz cfg:foo=a,bar=b env:OUTDIR=path/to,OTHER=foo
//! fn insert_source_code_here() {}
//! "
//! ```
use std::{str::FromStr, sync::Arc};
use cfg::CfgOptions;
use rustc_hash::FxHashMap;
use test_utils::{extract_range_or_offset, Fixture, RangeOrOffset, CURSOR_MARKER};
use vfs::{file_set::FileSet, VfsPath};
use crate::{
input::CrateName, CrateGraph, CrateId, Edition, Env, FileId, FilePosition, SourceDatabaseExt,
SourceRoot, SourceRootId,
};
pub const WORKSPACE: SourceRootId = SourceRootId(0);
pub trait WithFixture: Default + SourceDatabaseExt + 'static {
fn with_single_file(text: &str) -> (Self, FileId) {
let mut db = Self::default();
let (_, files) = with_files(&mut db, text);
assert_eq!(files.len(), 1);
(db, files[0])
}
fn with_files(ra_fixture: &str) -> Self {
let mut db = Self::default();
let (pos, _) = with_files(&mut db, ra_fixture);
assert!(pos.is_none());
db
}
fn with_position(ra_fixture: &str) -> (Self, FilePosition) {
let (db, file_id, range_or_offset) = Self::with_range_or_offset(ra_fixture);
let offset = match range_or_offset {
RangeOrOffset::Range(_) => panic!(),
RangeOrOffset::Offset(it) => it,
};
(db, FilePosition { file_id, offset })
}
fn with_range_or_offset(ra_fixture: &str) -> (Self, FileId, RangeOrOffset) {
let mut db = Self::default();
let (pos, _) = with_files(&mut db, ra_fixture);
let (file_id, range_or_offset) = pos.unwrap();
(db, file_id, range_or_offset)
}
fn test_crate(&self) -> CrateId {
let crate_graph = self.crate_graph();
let mut it = crate_graph.iter();
let res = it.next().unwrap();
assert!(it.next().is_none());
res
}
}
impl<DB: SourceDatabaseExt + Default + 'static> WithFixture for DB {}
fn with_files(
db: &mut dyn SourceDatabaseExt,
fixture: &str,
) -> (Option<(FileId, RangeOrOffset)>, Vec<FileId>) {
let fixture = Fixture::parse(fixture);
let mut files = Vec::new();
let mut crate_graph = CrateGraph::default();
let mut crates = FxHashMap::default();
let mut crate_deps = Vec::new();
let mut default_crate_root: Option<FileId> = None;
let mut file_set = FileSet::default();
let source_root_id = WORKSPACE;
let source_root_prefix = "/".to_string();
let mut file_id = FileId(0);
let mut file_position = None;
for entry in fixture {
let text = if entry.text.contains(CURSOR_MARKER) {
let (range_or_offset, text) = extract_range_or_offset(&entry.text);
assert!(file_position.is_none());
file_position = Some((file_id, range_or_offset));
text.to_string()
} else {
entry.text.clone()
};
let meta = FileMeta::from(entry);
assert!(meta.path.starts_with(&source_root_prefix));
if let Some(krate) = meta.krate {
let crate_id = crate_graph.add_crate_root(
file_id,
meta.edition,
Some(krate.clone()),
meta.cfg,
meta.env,
Default::default(),
);
let crate_name = CrateName::new(&krate).unwrap();
let prev = crates.insert(crate_name.clone(), crate_id);
assert!(prev.is_none());
for dep in meta.deps {
let dep = CrateName::new(&dep).unwrap();
crate_deps.push((crate_name.clone(), dep))
}
} else if meta.path == "/main.rs" || meta.path == "/lib.rs" {
assert!(default_crate_root.is_none());
default_crate_root = Some(file_id);
}
db.set_file_text(file_id, Arc::new(text));
db.set_file_source_root(file_id, source_root_id);
let path = VfsPath::new_virtual_path(meta.path);
file_set.insert(file_id, path.into());
files.push(file_id);
file_id.0 += 1;
}
if crates.is_empty() {
let crate_root = default_crate_root.unwrap();
crate_graph.add_crate_root(
crate_root,
Edition::Edition2018,
None,
CfgOptions::default(),
Env::default(),
Default::default(),
);
} else {
for (from, to) in crate_deps {
let from_id = crates[&from];
let to_id = crates[&to];
crate_graph.add_dep(from_id, CrateName::new(&to).unwrap(), to_id).unwrap();
}
}
db.set_source_root(source_root_id, Arc::new(SourceRoot::new_local(file_set)));
db.set_crate_graph(Arc::new(crate_graph));
(file_position, files)
}
struct FileMeta {
path: String,
krate: Option<String>,
deps: Vec<String>,
cfg: CfgOptions,
edition: Edition,
env: Env,
}
impl From<Fixture> for FileMeta {
fn from(f: Fixture) -> FileMeta {
let mut cfg = CfgOptions::default();
f.cfg_atoms.iter().for_each(|it| cfg.insert_atom(it.into()));
f.cfg_key_values.iter().for_each(|(k, v)| cfg.insert_key_value(k.into(), v.into()));
FileMeta {
path: f.path,
krate: f.krate,
deps: f.deps,
cfg,
edition: f
.edition
.as_ref()
.map_or(Edition::Edition2018, |v| Edition::from_str(&v).unwrap()),
env: f.env.into_iter().collect(),
}
}
}

453
crates/base_db/src/input.rs Normal file
View file

@ -0,0 +1,453 @@
//! This module specifies the input to rust-analyzer. In some sense, this is
//! **the** most important module, because all other fancy stuff is strictly
//! derived from this input.
//!
//! Note that neither this module, nor any other part of the analyzer's core do
//! actual IO. See `vfs` and `project_model` in the `rust-analyzer` crate for how
//! actual IO is done and lowered to input.
use std::{fmt, iter::FromIterator, ops, str::FromStr, sync::Arc};
use cfg::CfgOptions;
use rustc_hash::{FxHashMap, FxHashSet};
use syntax::SmolStr;
use tt::TokenExpander;
use vfs::file_set::FileSet;
pub use vfs::FileId;
/// Files are grouped into source roots. A source root is a directory on the
/// file systems which is watched for changes. Typically it corresponds to a
/// Rust crate. Source roots *might* be nested: in this case, a file belongs to
/// the nearest enclosing source root. Paths to files are always relative to a
/// source root, and the analyzer does not know the root path of the source root at
/// all. So, a file from one source root can't refer to a file in another source
/// root by path.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct SourceRootId(pub u32);
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SourceRoot {
/// Sysroot or crates.io library.
///
/// Libraries are considered mostly immutable, this assumption is used to
/// optimize salsa's query structure
pub is_library: bool,
pub(crate) file_set: FileSet,
}
impl SourceRoot {
pub fn new_local(file_set: FileSet) -> SourceRoot {
SourceRoot { is_library: false, file_set }
}
pub fn new_library(file_set: FileSet) -> SourceRoot {
SourceRoot { is_library: true, file_set }
}
pub fn iter(&self) -> impl Iterator<Item = FileId> + '_ {
self.file_set.iter()
}
}
/// `CrateGraph` is a bit of information which turns a set of text files into a
/// number of Rust crates. Each crate is defined by the `FileId` of its root module,
/// the set of cfg flags (not yet implemented) and the set of dependencies. Note
/// that, due to cfg's, there might be several crates for a single `FileId`! As
/// in the rust-lang proper, a crate does not have a name. Instead, names are
/// specified on dependency edges. That is, a crate might be known under
/// different names in different dependent crates.
///
/// Note that `CrateGraph` is build-system agnostic: it's a concept of the Rust
/// language proper, not a concept of the build system. In practice, we get
/// `CrateGraph` by lowering `cargo metadata` output.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct CrateGraph {
arena: FxHashMap<CrateId, CrateData>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct CrateId(pub u32);
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct CrateName(SmolStr);
impl CrateName {
/// Creates a crate name, checking for dashes in the string provided.
/// Dashes are not allowed in the crate names,
/// hence the input string is returned as `Err` for those cases.
pub fn new(name: &str) -> Result<CrateName, &str> {
if name.contains('-') {
Err(name)
} else {
Ok(Self(SmolStr::new(name)))
}
}
/// Creates a crate name, unconditionally replacing the dashes with underscores.
pub fn normalize_dashes(name: &str) -> CrateName {
Self(SmolStr::new(name.replace('-', "_")))
}
}
impl fmt::Display for CrateName {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl ops::Deref for CrateName {
type Target = str;
fn deref(&self) -> &Self::Target {
&*self.0
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub struct ProcMacroId(pub u32);
#[derive(Debug, Clone)]
pub struct ProcMacro {
pub name: SmolStr,
pub expander: Arc<dyn TokenExpander>,
}
impl Eq for ProcMacro {}
impl PartialEq for ProcMacro {
fn eq(&self, other: &ProcMacro) -> bool {
self.name == other.name && Arc::ptr_eq(&self.expander, &other.expander)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CrateData {
pub root_file_id: FileId,
pub edition: Edition,
/// The name to display to the end user.
/// This actual crate name can be different in a particular dependent crate
/// or may even be missing for some cases, such as a dummy crate for the code snippet.
pub display_name: Option<String>,
pub cfg_options: CfgOptions,
pub env: Env,
pub dependencies: Vec<Dependency>,
pub proc_macro: Vec<ProcMacro>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Edition {
Edition2018,
Edition2015,
}
#[derive(Default, Debug, Clone, PartialEq, Eq)]
pub struct Env {
entries: FxHashMap<String, String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Dependency {
pub crate_id: CrateId,
pub name: CrateName,
}
impl CrateGraph {
pub fn add_crate_root(
&mut self,
file_id: FileId,
edition: Edition,
display_name: Option<String>,
cfg_options: CfgOptions,
env: Env,
proc_macro: Vec<(SmolStr, Arc<dyn tt::TokenExpander>)>,
) -> CrateId {
let proc_macro =
proc_macro.into_iter().map(|(name, it)| ProcMacro { name, expander: it }).collect();
let data = CrateData {
root_file_id: file_id,
edition,
display_name,
cfg_options,
env,
proc_macro,
dependencies: Vec::new(),
};
let crate_id = CrateId(self.arena.len() as u32);
let prev = self.arena.insert(crate_id, data);
assert!(prev.is_none());
crate_id
}
pub fn add_dep(
&mut self,
from: CrateId,
name: CrateName,
to: CrateId,
) -> Result<(), CyclicDependenciesError> {
if self.dfs_find(from, to, &mut FxHashSet::default()) {
return Err(CyclicDependenciesError);
}
self.arena.get_mut(&from).unwrap().add_dep(name, to);
Ok(())
}
pub fn is_empty(&self) -> bool {
self.arena.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = CrateId> + '_ {
self.arena.keys().copied()
}
/// Returns an iterator over all transitive dependencies of the given crate.
pub fn transitive_deps(&self, of: CrateId) -> impl Iterator<Item = CrateId> + '_ {
let mut worklist = vec![of];
let mut deps = FxHashSet::default();
while let Some(krate) = worklist.pop() {
if !deps.insert(krate) {
continue;
}
worklist.extend(self[krate].dependencies.iter().map(|dep| dep.crate_id));
}
deps.remove(&of);
deps.into_iter()
}
// FIXME: this only finds one crate with the given root; we could have multiple
pub fn crate_id_for_crate_root(&self, file_id: FileId) -> Option<CrateId> {
let (&crate_id, _) =
self.arena.iter().find(|(_crate_id, data)| data.root_file_id == file_id)?;
Some(crate_id)
}
/// Extends this crate graph by adding a complete disjoint second crate
/// graph.
///
/// The ids of the crates in the `other` graph are shifted by the return
/// amount.
pub fn extend(&mut self, other: CrateGraph) -> u32 {
let start = self.arena.len() as u32;
self.arena.extend(other.arena.into_iter().map(|(id, mut data)| {
let new_id = id.shift(start);
for dep in &mut data.dependencies {
dep.crate_id = dep.crate_id.shift(start);
}
(new_id, data)
}));
start
}
fn dfs_find(&self, target: CrateId, from: CrateId, visited: &mut FxHashSet<CrateId>) -> bool {
if !visited.insert(from) {
return false;
}
if target == from {
return true;
}
for dep in &self[from].dependencies {
let crate_id = dep.crate_id;
if self.dfs_find(target, crate_id, visited) {
return true;
}
}
false
}
}
impl ops::Index<CrateId> for CrateGraph {
type Output = CrateData;
fn index(&self, crate_id: CrateId) -> &CrateData {
&self.arena[&crate_id]
}
}
impl CrateId {
pub fn shift(self, amount: u32) -> CrateId {
CrateId(self.0 + amount)
}
}
impl CrateData {
fn add_dep(&mut self, name: CrateName, crate_id: CrateId) {
self.dependencies.push(Dependency { name, crate_id })
}
}
impl FromStr for Edition {
type Err = ParseEditionError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let res = match s {
"2015" => Edition::Edition2015,
"2018" => Edition::Edition2018,
_ => return Err(ParseEditionError { invalid_input: s.to_string() }),
};
Ok(res)
}
}
impl fmt::Display for Edition {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Edition::Edition2015 => "2015",
Edition::Edition2018 => "2018",
})
}
}
impl FromIterator<(String, String)> for Env {
fn from_iter<T: IntoIterator<Item = (String, String)>>(iter: T) -> Self {
Env { entries: FromIterator::from_iter(iter) }
}
}
impl Env {
pub fn set(&mut self, env: &str, value: String) {
self.entries.insert(env.to_owned(), value);
}
pub fn get(&self, env: &str) -> Option<String> {
self.entries.get(env).cloned()
}
}
#[derive(Debug)]
pub struct ParseEditionError {
invalid_input: String,
}
impl fmt::Display for ParseEditionError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "invalid edition: {:?}", self.invalid_input)
}
}
impl std::error::Error for ParseEditionError {}
#[derive(Debug)]
pub struct CyclicDependenciesError;
#[cfg(test)]
mod tests {
use super::{CfgOptions, CrateGraph, CrateName, Dependency, Edition::Edition2018, Env, FileId};
#[test]
fn detect_cyclic_dependency_indirect() {
let mut graph = CrateGraph::default();
let crate1 = graph.add_crate_root(
FileId(1u32),
Edition2018,
None,
CfgOptions::default(),
Env::default(),
Default::default(),
);
let crate2 = graph.add_crate_root(
FileId(2u32),
Edition2018,
None,
CfgOptions::default(),
Env::default(),
Default::default(),
);
let crate3 = graph.add_crate_root(
FileId(3u32),
Edition2018,
None,
CfgOptions::default(),
Env::default(),
Default::default(),
);
assert!(graph.add_dep(crate1, CrateName::new("crate2").unwrap(), crate2).is_ok());
assert!(graph.add_dep(crate2, CrateName::new("crate3").unwrap(), crate3).is_ok());
assert!(graph.add_dep(crate3, CrateName::new("crate1").unwrap(), crate1).is_err());
}
#[test]
fn detect_cyclic_dependency_direct() {
let mut graph = CrateGraph::default();
let crate1 = graph.add_crate_root(
FileId(1u32),
Edition2018,
None,
CfgOptions::default(),
Env::default(),
Default::default(),
);
let crate2 = graph.add_crate_root(
FileId(2u32),
Edition2018,
None,
CfgOptions::default(),
Env::default(),
Default::default(),
);
assert!(graph.add_dep(crate1, CrateName::new("crate2").unwrap(), crate2).is_ok());
assert!(graph.add_dep(crate2, CrateName::new("crate2").unwrap(), crate2).is_err());
}
#[test]
fn it_works() {
let mut graph = CrateGraph::default();
let crate1 = graph.add_crate_root(
FileId(1u32),
Edition2018,
None,
CfgOptions::default(),
Env::default(),
Default::default(),
);
let crate2 = graph.add_crate_root(
FileId(2u32),
Edition2018,
None,
CfgOptions::default(),
Env::default(),
Default::default(),
);
let crate3 = graph.add_crate_root(
FileId(3u32),
Edition2018,
None,
CfgOptions::default(),
Env::default(),
Default::default(),
);
assert!(graph.add_dep(crate1, CrateName::new("crate2").unwrap(), crate2).is_ok());
assert!(graph.add_dep(crate2, CrateName::new("crate3").unwrap(), crate3).is_ok());
}
#[test]
fn dashes_are_normalized() {
let mut graph = CrateGraph::default();
let crate1 = graph.add_crate_root(
FileId(1u32),
Edition2018,
None,
CfgOptions::default(),
Env::default(),
Default::default(),
);
let crate2 = graph.add_crate_root(
FileId(2u32),
Edition2018,
None,
CfgOptions::default(),
Env::default(),
Default::default(),
);
assert!(graph
.add_dep(crate1, CrateName::normalize_dashes("crate-name-with-dashes"), crate2)
.is_ok());
assert_eq!(
graph[crate1].dependencies,
vec![Dependency {
crate_id: crate2,
name: CrateName::new("crate_name_with_dashes").unwrap()
}]
);
}
}

167
crates/base_db/src/lib.rs Normal file
View file

@ -0,0 +1,167 @@
//! base_db defines basic database traits. The concrete DB is defined by ide.
mod cancellation;
mod input;
pub mod fixture;
use std::{panic, sync::Arc};
use rustc_hash::FxHashSet;
use syntax::{ast, Parse, SourceFile, TextRange, TextSize};
pub use crate::{
cancellation::Canceled,
input::{
CrateData, CrateGraph, CrateId, CrateName, Dependency, Edition, Env, FileId, ProcMacroId,
SourceRoot, SourceRootId,
},
};
pub use salsa;
pub use vfs::{file_set::FileSet, VfsPath};
#[macro_export]
macro_rules! impl_intern_key {
($name:ident) => {
impl $crate::salsa::InternKey for $name {
fn from_intern_id(v: $crate::salsa::InternId) -> Self {
$name(v)
}
fn as_intern_id(&self) -> $crate::salsa::InternId {
self.0
}
}
};
}
pub trait Upcast<T: ?Sized> {
fn upcast(&self) -> &T;
}
pub trait CheckCanceled {
/// Aborts current query if there are pending changes.
///
/// rust-analyzer needs to be able to answer semantic questions about the
/// code while the code is being modified. A common problem is that a
/// long-running query is being calculated when a new change arrives.
///
/// We can't just apply the change immediately: this will cause the pending
/// query to see inconsistent state (it will observe an absence of
/// repeatable read). So what we do is we **cancel** all pending queries
/// before applying the change.
///
/// We implement cancellation by panicking with a special value and catching
/// it on the API boundary. Salsa explicitly supports this use-case.
fn check_canceled(&self);
fn catch_canceled<F, T>(&self, f: F) -> Result<T, Canceled>
where
Self: Sized + panic::RefUnwindSafe,
F: FnOnce(&Self) -> T + panic::UnwindSafe,
{
panic::catch_unwind(|| f(self)).map_err(|err| match err.downcast::<Canceled>() {
Ok(canceled) => *canceled,
Err(payload) => panic::resume_unwind(payload),
})
}
}
impl<T: salsa::Database> CheckCanceled for T {
fn check_canceled(&self) {
if self.salsa_runtime().is_current_revision_canceled() {
Canceled::throw()
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct FilePosition {
pub file_id: FileId,
pub offset: TextSize,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct FileRange {
pub file_id: FileId,
pub range: TextRange,
}
pub const DEFAULT_LRU_CAP: usize = 128;
pub trait FileLoader {
/// Text of the file.
fn file_text(&self, file_id: FileId) -> Arc<String>;
/// Note that we intentionally accept a `&str` and not a `&Path` here. This
/// method exists to handle `#[path = "/some/path.rs"] mod foo;` and such,
/// so the input is guaranteed to be utf-8 string. One might be tempted to
/// introduce some kind of "utf-8 path with / separators", but that's a bad idea. Behold
/// `#[path = "C://no/way"]`
fn resolve_path(&self, anchor: FileId, path: &str) -> Option<FileId>;
fn relevant_crates(&self, file_id: FileId) -> Arc<FxHashSet<CrateId>>;
}
/// Database which stores all significant input facts: source code and project
/// model. Everything else in rust-analyzer is derived from these queries.
#[salsa::query_group(SourceDatabaseStorage)]
pub trait SourceDatabase: CheckCanceled + FileLoader + std::fmt::Debug {
// Parses the file into the syntax tree.
#[salsa::invoke(parse_query)]
fn parse(&self, file_id: FileId) -> Parse<ast::SourceFile>;
/// The crate graph.
#[salsa::input]
fn crate_graph(&self) -> Arc<CrateGraph>;
}
fn parse_query(db: &dyn SourceDatabase, file_id: FileId) -> Parse<ast::SourceFile> {
let _p = profile::span("parse_query").detail(|| format!("{:?}", file_id));
let text = db.file_text(file_id);
SourceFile::parse(&*text)
}
/// We don't want to give HIR knowledge of source roots, hence we extract these
/// methods into a separate DB.
#[salsa::query_group(SourceDatabaseExtStorage)]
pub trait SourceDatabaseExt: SourceDatabase {
#[salsa::input]
fn file_text(&self, file_id: FileId) -> Arc<String>;
/// Path to a file, relative to the root of its source root.
/// Source root of the file.
#[salsa::input]
fn file_source_root(&self, file_id: FileId) -> SourceRootId;
/// Contents of the source root.
#[salsa::input]
fn source_root(&self, id: SourceRootId) -> Arc<SourceRoot>;
fn source_root_crates(&self, id: SourceRootId) -> Arc<FxHashSet<CrateId>>;
}
fn source_root_crates(db: &dyn SourceDatabaseExt, id: SourceRootId) -> Arc<FxHashSet<CrateId>> {
let graph = db.crate_graph();
let res = graph
.iter()
.filter(|&krate| {
let root_file = graph[krate].root_file_id;
db.file_source_root(root_file) == id
})
.collect::<FxHashSet<_>>();
Arc::new(res)
}
/// Silly workaround for cyclic deps between the traits
pub struct FileLoaderDelegate<T>(pub T);
impl<T: SourceDatabaseExt> FileLoader for FileLoaderDelegate<&'_ T> {
fn file_text(&self, file_id: FileId) -> Arc<String> {
SourceDatabaseExt::file_text(self.0, file_id)
}
fn resolve_path(&self, anchor: FileId, path: &str) -> Option<FileId> {
// FIXME: this *somehow* should be platform agnostic...
let source_root = self.0.file_source_root(anchor);
let source_root = self.0.source_root(source_root);
source_root.file_set.resolve_path(anchor, path)
}
fn relevant_crates(&self, file_id: FileId) -> Arc<FxHashSet<CrateId>> {
let source_root = self.0.file_source_root(file_id);
self.0.source_root_crates(source_root)
}
}

18
crates/cfg/Cargo.toml Normal file
View file

@ -0,0 +1,18 @@
[package]
name = "cfg"
version = "0.0.0"
license = "MIT OR Apache-2.0"
authors = ["rust-analyzer developers"]
edition = "2018"
[lib]
doctest = false
[dependencies]
rustc-hash = "1.1.0"
tt = { path = "../tt" }
[dev-dependencies]
mbe = { path = "../mbe" }
syntax = { path = "../syntax" }

133
crates/cfg/src/cfg_expr.rs Normal file
View file

@ -0,0 +1,133 @@
//! The condition expression used in `#[cfg(..)]` attributes.
//!
//! See: https://doc.rust-lang.org/reference/conditional-compilation.html#conditional-compilation
use std::slice::Iter as SliceIter;
use tt::SmolStr;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CfgExpr {
Invalid,
Atom(SmolStr),
KeyValue { key: SmolStr, value: SmolStr },
All(Vec<CfgExpr>),
Any(Vec<CfgExpr>),
Not(Box<CfgExpr>),
}
impl CfgExpr {
pub fn parse(tt: &tt::Subtree) -> CfgExpr {
next_cfg_expr(&mut tt.token_trees.iter()).unwrap_or(CfgExpr::Invalid)
}
/// Fold the cfg by querying all basic `Atom` and `KeyValue` predicates.
pub fn fold(&self, query: &dyn Fn(&SmolStr, Option<&SmolStr>) -> bool) -> Option<bool> {
match self {
CfgExpr::Invalid => None,
CfgExpr::Atom(name) => Some(query(name, None)),
CfgExpr::KeyValue { key, value } => Some(query(key, Some(value))),
CfgExpr::All(preds) => {
preds.iter().try_fold(true, |s, pred| Some(s && pred.fold(query)?))
}
CfgExpr::Any(preds) => {
preds.iter().try_fold(false, |s, pred| Some(s || pred.fold(query)?))
}
CfgExpr::Not(pred) => pred.fold(query).map(|s| !s),
}
}
}
fn next_cfg_expr(it: &mut SliceIter<tt::TokenTree>) -> Option<CfgExpr> {
let name = match it.next() {
None => return None,
Some(tt::TokenTree::Leaf(tt::Leaf::Ident(ident))) => ident.text.clone(),
Some(_) => return Some(CfgExpr::Invalid),
};
// Peek
let ret = match it.as_slice().first() {
Some(tt::TokenTree::Leaf(tt::Leaf::Punct(punct))) if punct.char == '=' => {
match it.as_slice().get(1) {
Some(tt::TokenTree::Leaf(tt::Leaf::Literal(literal))) => {
it.next();
it.next();
// FIXME: escape? raw string?
let value =
SmolStr::new(literal.text.trim_start_matches('"').trim_end_matches('"'));
CfgExpr::KeyValue { key: name, value }
}
_ => return Some(CfgExpr::Invalid),
}
}
Some(tt::TokenTree::Subtree(subtree)) => {
it.next();
let mut sub_it = subtree.token_trees.iter();
let mut subs = std::iter::from_fn(|| next_cfg_expr(&mut sub_it)).collect();
match name.as_str() {
"all" => CfgExpr::All(subs),
"any" => CfgExpr::Any(subs),
"not" => CfgExpr::Not(Box::new(subs.pop().unwrap_or(CfgExpr::Invalid))),
_ => CfgExpr::Invalid,
}
}
_ => CfgExpr::Atom(name),
};
// Eat comma separator
if let Some(tt::TokenTree::Leaf(tt::Leaf::Punct(punct))) = it.as_slice().first() {
if punct.char == ',' {
it.next();
}
}
Some(ret)
}
#[cfg(test)]
mod tests {
use super::*;
use mbe::ast_to_token_tree;
use syntax::ast::{self, AstNode};
fn assert_parse_result(input: &str, expected: CfgExpr) {
let (tt, _) = {
let source_file = ast::SourceFile::parse(input).ok().unwrap();
let tt = source_file.syntax().descendants().find_map(ast::TokenTree::cast).unwrap();
ast_to_token_tree(&tt).unwrap()
};
let cfg = CfgExpr::parse(&tt);
assert_eq!(cfg, expected);
}
#[test]
fn test_cfg_expr_parser() {
assert_parse_result("#![cfg(foo)]", CfgExpr::Atom("foo".into()));
assert_parse_result("#![cfg(foo,)]", CfgExpr::Atom("foo".into()));
assert_parse_result(
"#![cfg(not(foo))]",
CfgExpr::Not(Box::new(CfgExpr::Atom("foo".into()))),
);
assert_parse_result("#![cfg(foo(bar))]", CfgExpr::Invalid);
// Only take the first
assert_parse_result(r#"#![cfg(foo, bar = "baz")]"#, CfgExpr::Atom("foo".into()));
assert_parse_result(
r#"#![cfg(all(foo, bar = "baz"))]"#,
CfgExpr::All(vec![
CfgExpr::Atom("foo".into()),
CfgExpr::KeyValue { key: "bar".into(), value: "baz".into() },
]),
);
assert_parse_result(
r#"#![cfg(any(not(), all(), , bar = "baz",))]"#,
CfgExpr::Any(vec![
CfgExpr::Not(Box::new(CfgExpr::Invalid)),
CfgExpr::All(vec![]),
CfgExpr::Invalid,
CfgExpr::KeyValue { key: "bar".into(), value: "baz".into() },
]),
);
}
}

51
crates/cfg/src/lib.rs Normal file
View file

@ -0,0 +1,51 @@
//! cfg defines conditional compiling options, `cfg` attibute parser and evaluator
mod cfg_expr;
use rustc_hash::FxHashSet;
use tt::SmolStr;
pub use cfg_expr::CfgExpr;
/// Configuration options used for conditional compilition on items with `cfg` attributes.
/// We have two kind of options in different namespaces: atomic options like `unix`, and
/// key-value options like `target_arch="x86"`.
///
/// Note that for key-value options, one key can have multiple values (but not none).
/// `feature` is an example. We have both `feature="foo"` and `feature="bar"` if features
/// `foo` and `bar` are both enabled. And here, we store key-value options as a set of tuple
/// of key and value in `key_values`.
///
/// See: https://doc.rust-lang.org/reference/conditional-compilation.html#set-configuration-options
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct CfgOptions {
atoms: FxHashSet<SmolStr>,
key_values: FxHashSet<(SmolStr, SmolStr)>,
}
impl CfgOptions {
pub fn check(&self, cfg: &CfgExpr) -> Option<bool> {
cfg.fold(&|key, value| match value {
None => self.atoms.contains(key),
Some(value) => self.key_values.contains(&(key.clone(), value.clone())),
})
}
pub fn insert_atom(&mut self, key: SmolStr) {
self.atoms.insert(key);
}
pub fn insert_key_value(&mut self, key: SmolStr, value: SmolStr) {
self.key_values.insert((key, value));
}
pub fn append(&mut self, other: &CfgOptions) {
for atom in &other.atoms {
self.atoms.insert(atom.clone());
}
for (key, value) in &other.key_values {
self.key_values.insert((key.clone(), value.clone()));
}
}
}

View file

@ -1,14 +0,0 @@
[package]
name = "expect"
version = "0.1.0"
authors = ["rust-analyzer developers"]
edition = "2018"
license = "MIT OR Apache-2.0"
[lib]
doctest = false
[dependencies]
once_cell = "1"
difference = "2"
stdx = { path = "../stdx" }

View file

@ -1,356 +0,0 @@
//! Snapshot testing library, see
//! https://github.com/rust-analyzer/rust-analyzer/pull/5101
use std::{
collections::HashMap,
env, fmt, fs, mem,
ops::Range,
panic,
path::{Path, PathBuf},
sync::Mutex,
};
use difference::Changeset;
use once_cell::sync::Lazy;
use stdx::{lines_with_ends, trim_indent};
const HELP: &str = "
You can update all `expect![[]]` tests by running:
env UPDATE_EXPECT=1 cargo test
To update a single test, place the cursor on `expect` token and use `run` feature of rust-analyzer.
";
fn update_expect() -> bool {
env::var("UPDATE_EXPECT").is_ok()
}
/// expect![[r#"inline snapshot"#]]
#[macro_export]
macro_rules! expect {
[[$data:literal]] => {$crate::Expect {
position: $crate::Position {
file: file!(),
line: line!(),
column: column!(),
},
data: $data,
}};
[[]] => { $crate::expect![[""]] };
}
/// expect_file!["/crates/foo/test_data/bar.html"]
#[macro_export]
macro_rules! expect_file {
[$path:expr] => {$crate::ExpectFile {
path: std::path::PathBuf::from($path)
}};
}
#[derive(Debug)]
pub struct Expect {
pub position: Position,
pub data: &'static str,
}
#[derive(Debug)]
pub struct ExpectFile {
pub path: PathBuf,
}
#[derive(Debug)]
pub struct Position {
pub file: &'static str,
pub line: u32,
pub column: u32,
}
impl fmt::Display for Position {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}:{}:{}", self.file, self.line, self.column)
}
}
impl Expect {
pub fn assert_eq(&self, actual: &str) {
let trimmed = self.trimmed();
if &trimmed == actual {
return;
}
Runtime::fail_expect(self, &trimmed, actual);
}
pub fn assert_debug_eq(&self, actual: &impl fmt::Debug) {
let actual = format!("{:#?}\n", actual);
self.assert_eq(&actual)
}
fn trimmed(&self) -> String {
if !self.data.contains('\n') {
return self.data.to_string();
}
trim_indent(self.data)
}
fn locate(&self, file: &str) -> Location {
let mut target_line = None;
let mut line_start = 0;
for (i, line) in lines_with_ends(file).enumerate() {
if i == self.position.line as usize - 1 {
let pat = "expect![[";
let offset = line.find(pat).unwrap();
let literal_start = line_start + offset + pat.len();
let indent = line.chars().take_while(|&it| it == ' ').count();
target_line = Some((literal_start, indent));
break;
}
line_start += line.len();
}
let (literal_start, line_indent) = target_line.unwrap();
let literal_length =
file[literal_start..].find("]]").expect("Couldn't find matching `]]` for `expect![[`.");
let literal_range = literal_start..literal_start + literal_length;
Location { line_indent, literal_range }
}
}
impl ExpectFile {
pub fn assert_eq(&self, actual: &str) {
let expected = self.read();
if actual == expected {
return;
}
Runtime::fail_file(self, &expected, actual);
}
pub fn assert_debug_eq(&self, actual: &impl fmt::Debug) {
let actual = format!("{:#?}\n", actual);
self.assert_eq(&actual)
}
fn read(&self) -> String {
fs::read_to_string(self.abs_path()).unwrap_or_default().replace("\r\n", "\n")
}
fn write(&self, contents: &str) {
fs::write(self.abs_path(), contents).unwrap()
}
fn abs_path(&self) -> PathBuf {
WORKSPACE_ROOT.join(&self.path)
}
}
#[derive(Default)]
struct Runtime {
help_printed: bool,
per_file: HashMap<&'static str, FileRuntime>,
}
static RT: Lazy<Mutex<Runtime>> = Lazy::new(Default::default);
impl Runtime {
fn fail_expect(expect: &Expect, expected: &str, actual: &str) {
let mut rt = RT.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
if update_expect() {
println!("\x1b[1m\x1b[92mupdating\x1b[0m: {}", expect.position);
rt.per_file
.entry(expect.position.file)
.or_insert_with(|| FileRuntime::new(expect))
.update(expect, actual);
return;
}
rt.panic(expect.position.to_string(), expected, actual);
}
fn fail_file(expect: &ExpectFile, expected: &str, actual: &str) {
let mut rt = RT.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
if update_expect() {
println!("\x1b[1m\x1b[92mupdating\x1b[0m: {}", expect.path.display());
expect.write(actual);
return;
}
rt.panic(expect.path.display().to_string(), expected, actual);
}
fn panic(&mut self, position: String, expected: &str, actual: &str) {
let print_help = !mem::replace(&mut self.help_printed, true);
let help = if print_help { HELP } else { "" };
let diff = Changeset::new(actual, expected, "\n");
println!(
"\n
\x1b[1m\x1b[91merror\x1b[97m: expect test failed\x1b[0m
\x1b[1m\x1b[34m-->\x1b[0m {}
{}
\x1b[1mExpect\x1b[0m:
----
{}
----
\x1b[1mActual\x1b[0m:
----
{}
----
\x1b[1mDiff\x1b[0m:
----
{}
----
",
position, help, expected, actual, diff
);
// Use resume_unwind instead of panic!() to prevent a backtrace, which is unnecessary noise.
panic::resume_unwind(Box::new(()));
}
}
struct FileRuntime {
path: PathBuf,
original_text: String,
patchwork: Patchwork,
}
impl FileRuntime {
fn new(expect: &Expect) -> FileRuntime {
let path = WORKSPACE_ROOT.join(expect.position.file);
let original_text = fs::read_to_string(&path).unwrap();
let patchwork = Patchwork::new(original_text.clone());
FileRuntime { path, original_text, patchwork }
}
fn update(&mut self, expect: &Expect, actual: &str) {
let loc = expect.locate(&self.original_text);
let patch = format_patch(loc.line_indent.clone(), actual);
self.patchwork.patch(loc.literal_range, &patch);
fs::write(&self.path, &self.patchwork.text).unwrap()
}
}
#[derive(Debug)]
struct Location {
line_indent: usize,
literal_range: Range<usize>,
}
#[derive(Debug)]
struct Patchwork {
text: String,
indels: Vec<(Range<usize>, usize)>,
}
impl Patchwork {
fn new(text: String) -> Patchwork {
Patchwork { text, indels: Vec::new() }
}
fn patch(&mut self, mut range: Range<usize>, patch: &str) {
self.indels.push((range.clone(), patch.len()));
self.indels.sort_by_key(|(delete, _insert)| delete.start);
let (delete, insert) = self
.indels
.iter()
.take_while(|(delete, _)| delete.start < range.start)
.map(|(delete, insert)| (delete.end - delete.start, insert))
.fold((0usize, 0usize), |(x1, y1), (x2, y2)| (x1 + x2, y1 + y2));
for pos in &mut [&mut range.start, &mut range.end] {
**pos -= delete;
**pos += insert;
}
self.text.replace_range(range, &patch);
}
}
fn format_patch(line_indent: usize, patch: &str) -> String {
let mut max_hashes = 0;
let mut cur_hashes = 0;
for byte in patch.bytes() {
if byte != b'#' {
cur_hashes = 0;
continue;
}
cur_hashes += 1;
max_hashes = max_hashes.max(cur_hashes);
}
let hashes = &"#".repeat(max_hashes + 1);
let indent = &" ".repeat(line_indent);
let is_multiline = patch.contains('\n');
let mut buf = String::new();
buf.push('r');
buf.push_str(hashes);
buf.push('"');
if is_multiline {
buf.push('\n');
}
let mut final_newline = false;
for line in lines_with_ends(patch) {
if is_multiline && !line.trim().is_empty() {
buf.push_str(indent);
buf.push_str(" ");
}
buf.push_str(line);
final_newline = line.ends_with('\n');
}
if final_newline {
buf.push_str(indent);
}
buf.push('"');
buf.push_str(hashes);
buf
}
static WORKSPACE_ROOT: Lazy<PathBuf> = Lazy::new(|| {
let my_manifest =
env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| env!("CARGO_MANIFEST_DIR").to_owned());
// Heuristic, see https://github.com/rust-lang/cargo/issues/3946
Path::new(&my_manifest)
.ancestors()
.filter(|it| it.join("Cargo.toml").exists())
.last()
.unwrap()
.to_path_buf()
});
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_format_patch() {
let patch = format_patch(0, "hello\nworld\n");
expect![[r##"
r#"
hello
world
"#"##]]
.assert_eq(&patch);
let patch = format_patch(4, "single line");
expect![[r##"r#"single line"#"##]].assert_eq(&patch);
}
#[test]
fn test_patchwork() {
let mut patchwork = Patchwork::new("one two three".to_string());
patchwork.patch(4..7, "zwei");
patchwork.patch(0..3, "один");
patchwork.patch(8..13, "3");
expect![[r#"
Patchwork {
text: "один zwei 3",
indels: [
(
0..3,
8,
),
(
4..7,
4,
),
(
8..13,
1,
),
],
}
"#]]
.assert_debug_eq(&patchwork);
}
}

View file

@ -1,9 +1,9 @@
[package]
edition = "2018"
name = "flycheck"
version = "0.1.0"
authors = ["rust-analyzer developers"]
version = "0.0.0"
license = "MIT OR Apache-2.0"
authors = ["rust-analyzer developers"]
edition = "2018"
[lib]
doctest = false
@ -14,4 +14,5 @@ log = "0.4.8"
cargo_metadata = "0.11.1"
serde_json = "1.0.48"
jod-thread = "0.1.1"
ra_toolchain = { path = "../ra_toolchain" }
toolchain = { path = "../toolchain" }

View file

@ -1,4 +1,4 @@
//! cargo_check provides the functionality needed to run `cargo check` or
//! Flycheck provides the functionality needed to run `cargo check` or
//! another compatible command (f.x. clippy) in a background thread and provide
//! LSP diagnostics based on the output of the command.
@ -147,6 +147,12 @@ impl FlycheckActor {
// avoid busy-waiting.
let cargo_handle = self.cargo_handle.take().unwrap();
let res = cargo_handle.join();
if res.is_err() {
log::error!(
"Flycheck failed to run the following command: {:?}",
self.check_command()
)
}
self.send(Message::Progress(Progress::DidFinish(res)));
}
Event::CheckEvent(Some(message)) => match message {
@ -187,7 +193,7 @@ impl FlycheckActor {
extra_args,
features,
} => {
let mut cmd = Command::new(ra_toolchain::cargo());
let mut cmd = Command::new(toolchain::cargo());
cmd.arg(command);
cmd.args(&["--workspace", "--message-format=json", "--manifest-path"])
.arg(self.workspace_root.join("Cargo.toml"));

26
crates/hir/Cargo.toml Normal file
View file

@ -0,0 +1,26 @@
[package]
name = "hir"
version = "0.0.0"
license = "MIT OR Apache-2.0"
authors = ["rust-analyzer developers"]
edition = "2018"
[lib]
doctest = false
[dependencies]
log = "0.4.8"
rustc-hash = "1.1.0"
either = "1.5.3"
arrayvec = "0.5.1"
itertools = "0.9.0"
url = "2.1.1"
stdx = { path = "../stdx" }
syntax = { path = "../syntax" }
base_db = { path = "../base_db" }
profile = { path = "../profile" }
hir_expand = { path = "../hir_expand" }
hir_def = { path = "../hir_def" }
hir_ty = { path = "../hir_ty" }
tt = { path = "../tt" }

1891
crates/hir/src/code_model.rs Normal file

File diff suppressed because it is too large Load diff

21
crates/hir/src/db.rs Normal file
View file

@ -0,0 +1,21 @@
//! FIXME: write short doc here
pub use hir_def::db::{
AttrsQuery, BodyQuery, BodyWithSourceMapQuery, ConstDataQuery, CrateDefMapQueryQuery,
CrateLangItemsQuery, DefDatabase, DefDatabaseStorage, DocumentationQuery, EnumDataQuery,
ExprScopesQuery, FunctionDataQuery, GenericParamsQuery, ImplDataQuery, ImportMapQuery,
InternConstQuery, InternDatabase, InternDatabaseStorage, InternEnumQuery, InternFunctionQuery,
InternImplQuery, InternStaticQuery, InternStructQuery, InternTraitQuery, InternTypeAliasQuery,
InternUnionQuery, ItemTreeQuery, LangItemQuery, ModuleLangItemsQuery, StaticDataQuery,
StructDataQuery, TraitDataQuery, TypeAliasDataQuery, UnionDataQuery,
};
pub use hir_expand::db::{
AstDatabase, AstDatabaseStorage, AstIdMapQuery, InternEagerExpansionQuery, InternMacroQuery,
MacroArgTextQuery, MacroDefQuery, MacroExpandQuery, ParseMacroQuery,
};
pub use hir_ty::db::*;
#[test]
fn hir_database_is_object_safe() {
fn _assert_object_safe(_: &dyn HirDatabase) {}
}

View file

@ -0,0 +1,6 @@
//! FIXME: write short doc here
pub use hir_def::diagnostics::UnresolvedModule;
pub use hir_expand::diagnostics::{Diagnostic, DiagnosticSink, DiagnosticSinkBuilder};
pub use hir_ty::diagnostics::{
MismatchedArgCount, MissingFields, MissingMatchArms, MissingOkInTailExpr, NoSuchField,
};

247
crates/hir/src/from_id.rs Normal file
View file

@ -0,0 +1,247 @@
//! Utility module for converting between hir_def ids and code_model wrappers.
//!
//! It's unclear if we need this long-term, but it's definitelly useful while we
//! are splitting the hir.
use hir_def::{
expr::PatId, AdtId, AssocItemId, AttrDefId, DefWithBodyId, EnumVariantId, FieldId,
GenericDefId, ModuleDefId, VariantId,
};
use crate::{
code_model::ItemInNs, Adt, AssocItem, AttrDef, DefWithBody, EnumVariant, Field, GenericDef,
Local, MacroDef, ModuleDef, VariantDef,
};
macro_rules! from_id {
($(($id:path, $ty:path)),*) => {$(
impl From<$id> for $ty {
fn from(id: $id) -> $ty {
$ty { id }
}
}
impl From<$ty> for $id {
fn from(ty: $ty) -> $id {
ty.id
}
}
)*}
}
from_id![
(base_db::CrateId, crate::Crate),
(hir_def::ModuleId, crate::Module),
(hir_def::StructId, crate::Struct),
(hir_def::UnionId, crate::Union),
(hir_def::EnumId, crate::Enum),
(hir_def::TypeAliasId, crate::TypeAlias),
(hir_def::TraitId, crate::Trait),
(hir_def::StaticId, crate::Static),
(hir_def::ConstId, crate::Const),
(hir_def::FunctionId, crate::Function),
(hir_def::ImplId, crate::ImplDef),
(hir_def::TypeParamId, crate::TypeParam),
(hir_expand::MacroDefId, crate::MacroDef)
];
impl From<AdtId> for Adt {
fn from(id: AdtId) -> Self {
match id {
AdtId::StructId(it) => Adt::Struct(it.into()),
AdtId::UnionId(it) => Adt::Union(it.into()),
AdtId::EnumId(it) => Adt::Enum(it.into()),
}
}
}
impl From<Adt> for AdtId {
fn from(id: Adt) -> Self {
match id {
Adt::Struct(it) => AdtId::StructId(it.id),
Adt::Union(it) => AdtId::UnionId(it.id),
Adt::Enum(it) => AdtId::EnumId(it.id),
}
}
}
impl From<EnumVariantId> for EnumVariant {
fn from(id: EnumVariantId) -> Self {
EnumVariant { parent: id.parent.into(), id: id.local_id }
}
}
impl From<EnumVariant> for EnumVariantId {
fn from(def: EnumVariant) -> Self {
EnumVariantId { parent: def.parent.id, local_id: def.id }
}
}
impl From<ModuleDefId> for ModuleDef {
fn from(id: ModuleDefId) -> Self {
match id {
ModuleDefId::ModuleId(it) => ModuleDef::Module(it.into()),
ModuleDefId::FunctionId(it) => ModuleDef::Function(it.into()),
ModuleDefId::AdtId(it) => ModuleDef::Adt(it.into()),
ModuleDefId::EnumVariantId(it) => ModuleDef::EnumVariant(it.into()),
ModuleDefId::ConstId(it) => ModuleDef::Const(it.into()),
ModuleDefId::StaticId(it) => ModuleDef::Static(it.into()),
ModuleDefId::TraitId(it) => ModuleDef::Trait(it.into()),
ModuleDefId::TypeAliasId(it) => ModuleDef::TypeAlias(it.into()),
ModuleDefId::BuiltinType(it) => ModuleDef::BuiltinType(it),
}
}
}
impl From<ModuleDef> for ModuleDefId {
fn from(id: ModuleDef) -> Self {
match id {
ModuleDef::Module(it) => ModuleDefId::ModuleId(it.into()),
ModuleDef::Function(it) => ModuleDefId::FunctionId(it.into()),
ModuleDef::Adt(it) => ModuleDefId::AdtId(it.into()),
ModuleDef::EnumVariant(it) => ModuleDefId::EnumVariantId(it.into()),
ModuleDef::Const(it) => ModuleDefId::ConstId(it.into()),
ModuleDef::Static(it) => ModuleDefId::StaticId(it.into()),
ModuleDef::Trait(it) => ModuleDefId::TraitId(it.into()),
ModuleDef::TypeAlias(it) => ModuleDefId::TypeAliasId(it.into()),
ModuleDef::BuiltinType(it) => ModuleDefId::BuiltinType(it),
}
}
}
impl From<DefWithBody> for DefWithBodyId {
fn from(def: DefWithBody) -> Self {
match def {
DefWithBody::Function(it) => DefWithBodyId::FunctionId(it.id),
DefWithBody::Static(it) => DefWithBodyId::StaticId(it.id),
DefWithBody::Const(it) => DefWithBodyId::ConstId(it.id),
}
}
}
impl From<DefWithBodyId> for DefWithBody {
fn from(def: DefWithBodyId) -> Self {
match def {
DefWithBodyId::FunctionId(it) => DefWithBody::Function(it.into()),
DefWithBodyId::StaticId(it) => DefWithBody::Static(it.into()),
DefWithBodyId::ConstId(it) => DefWithBody::Const(it.into()),
}
}
}
impl From<AssocItemId> for AssocItem {
fn from(def: AssocItemId) -> Self {
match def {
AssocItemId::FunctionId(it) => AssocItem::Function(it.into()),
AssocItemId::TypeAliasId(it) => AssocItem::TypeAlias(it.into()),
AssocItemId::ConstId(it) => AssocItem::Const(it.into()),
}
}
}
impl From<GenericDef> for GenericDefId {
fn from(def: GenericDef) -> Self {
match def {
GenericDef::Function(it) => GenericDefId::FunctionId(it.id),
GenericDef::Adt(it) => GenericDefId::AdtId(it.into()),
GenericDef::Trait(it) => GenericDefId::TraitId(it.id),
GenericDef::TypeAlias(it) => GenericDefId::TypeAliasId(it.id),
GenericDef::ImplDef(it) => GenericDefId::ImplId(it.id),
GenericDef::EnumVariant(it) => {
GenericDefId::EnumVariantId(EnumVariantId { parent: it.parent.id, local_id: it.id })
}
GenericDef::Const(it) => GenericDefId::ConstId(it.id),
}
}
}
impl From<Adt> for GenericDefId {
fn from(id: Adt) -> Self {
match id {
Adt::Struct(it) => it.id.into(),
Adt::Union(it) => it.id.into(),
Adt::Enum(it) => it.id.into(),
}
}
}
impl From<VariantId> for VariantDef {
fn from(def: VariantId) -> Self {
match def {
VariantId::StructId(it) => VariantDef::Struct(it.into()),
VariantId::EnumVariantId(it) => VariantDef::EnumVariant(it.into()),
VariantId::UnionId(it) => VariantDef::Union(it.into()),
}
}
}
impl From<VariantDef> for VariantId {
fn from(def: VariantDef) -> Self {
match def {
VariantDef::Struct(it) => VariantId::StructId(it.id),
VariantDef::EnumVariant(it) => VariantId::EnumVariantId(it.into()),
VariantDef::Union(it) => VariantId::UnionId(it.id),
}
}
}
impl From<Field> for FieldId {
fn from(def: Field) -> Self {
FieldId { parent: def.parent.into(), local_id: def.id }
}
}
impl From<FieldId> for Field {
fn from(def: FieldId) -> Self {
Field { parent: def.parent.into(), id: def.local_id }
}
}
impl From<AttrDef> for AttrDefId {
fn from(def: AttrDef) -> Self {
match def {
AttrDef::Module(it) => AttrDefId::ModuleId(it.id),
AttrDef::Field(it) => AttrDefId::FieldId(it.into()),
AttrDef::Adt(it) => AttrDefId::AdtId(it.into()),
AttrDef::Function(it) => AttrDefId::FunctionId(it.id),
AttrDef::EnumVariant(it) => AttrDefId::EnumVariantId(it.into()),
AttrDef::Static(it) => AttrDefId::StaticId(it.id),
AttrDef::Const(it) => AttrDefId::ConstId(it.id),
AttrDef::Trait(it) => AttrDefId::TraitId(it.id),
AttrDef::TypeAlias(it) => AttrDefId::TypeAliasId(it.id),
AttrDef::MacroDef(it) => AttrDefId::MacroDefId(it.id),
}
}
}
impl From<AssocItem> for GenericDefId {
fn from(item: AssocItem) -> Self {
match item {
AssocItem::Function(f) => f.id.into(),
AssocItem::Const(c) => c.id.into(),
AssocItem::TypeAlias(t) => t.id.into(),
}
}
}
impl From<(DefWithBodyId, PatId)> for Local {
fn from((parent, pat_id): (DefWithBodyId, PatId)) -> Self {
Local { parent, pat_id }
}
}
impl From<MacroDef> for ItemInNs {
fn from(macro_def: MacroDef) -> Self {
ItemInNs::Macros(macro_def.into())
}
}
impl From<ModuleDef> for ItemInNs {
fn from(module_def: ModuleDef) -> Self {
match module_def {
ModuleDef::Static(_) | ModuleDef::Const(_) | ModuleDef::Function(_) => {
ItemInNs::Values(module_def.into())
}
_ => ItemInNs::Types(module_def.into()),
}
}
}

View file

@ -0,0 +1,135 @@
//! Provides set of implementation for hir's objects that allows get back location in file.
use either::Either;
use hir_def::{
nameres::{ModuleOrigin, ModuleSource},
src::{HasChildSource, HasSource as _},
Lookup, VariantId,
};
use syntax::ast;
use crate::{
db::HirDatabase, Const, Enum, EnumVariant, Field, FieldSource, Function, ImplDef, MacroDef,
Module, Static, Struct, Trait, TypeAlias, TypeParam, Union,
};
pub use hir_expand::InFile;
pub trait HasSource {
type Ast;
fn source(self, db: &dyn HirDatabase) -> InFile<Self::Ast>;
}
/// NB: Module is !HasSource, because it has two source nodes at the same time:
/// definition and declaration.
impl Module {
/// Returns a node which defines this module. That is, a file or a `mod foo {}` with items.
pub fn definition_source(self, db: &dyn HirDatabase) -> InFile<ModuleSource> {
let def_map = db.crate_def_map(self.id.krate);
def_map[self.id.local_id].definition_source(db.upcast())
}
pub fn is_mod_rs(self, db: &dyn HirDatabase) -> bool {
let def_map = db.crate_def_map(self.id.krate);
match def_map[self.id.local_id].origin {
ModuleOrigin::File { is_mod_rs, .. } => is_mod_rs,
_ => false,
}
}
/// Returns a node which declares this module, either a `mod foo;` or a `mod foo {}`.
/// `None` for the crate root.
pub fn declaration_source(self, db: &dyn HirDatabase) -> Option<InFile<ast::Module>> {
let def_map = db.crate_def_map(self.id.krate);
def_map[self.id.local_id].declaration_source(db.upcast())
}
}
impl HasSource for Field {
type Ast = FieldSource;
fn source(self, db: &dyn HirDatabase) -> InFile<FieldSource> {
let var = VariantId::from(self.parent);
let src = var.child_source(db.upcast());
src.map(|it| match it[self.id].clone() {
Either::Left(it) => FieldSource::Pos(it),
Either::Right(it) => FieldSource::Named(it),
})
}
}
impl HasSource for Struct {
type Ast = ast::Struct;
fn source(self, db: &dyn HirDatabase) -> InFile<ast::Struct> {
self.id.lookup(db.upcast()).source(db.upcast())
}
}
impl HasSource for Union {
type Ast = ast::Union;
fn source(self, db: &dyn HirDatabase) -> InFile<ast::Union> {
self.id.lookup(db.upcast()).source(db.upcast())
}
}
impl HasSource for Enum {
type Ast = ast::Enum;
fn source(self, db: &dyn HirDatabase) -> InFile<ast::Enum> {
self.id.lookup(db.upcast()).source(db.upcast())
}
}
impl HasSource for EnumVariant {
type Ast = ast::Variant;
fn source(self, db: &dyn HirDatabase) -> InFile<ast::Variant> {
self.parent.id.child_source(db.upcast()).map(|map| map[self.id].clone())
}
}
impl HasSource for Function {
type Ast = ast::Fn;
fn source(self, db: &dyn HirDatabase) -> InFile<ast::Fn> {
self.id.lookup(db.upcast()).source(db.upcast())
}
}
impl HasSource for Const {
type Ast = ast::Const;
fn source(self, db: &dyn HirDatabase) -> InFile<ast::Const> {
self.id.lookup(db.upcast()).source(db.upcast())
}
}
impl HasSource for Static {
type Ast = ast::Static;
fn source(self, db: &dyn HirDatabase) -> InFile<ast::Static> {
self.id.lookup(db.upcast()).source(db.upcast())
}
}
impl HasSource for Trait {
type Ast = ast::Trait;
fn source(self, db: &dyn HirDatabase) -> InFile<ast::Trait> {
self.id.lookup(db.upcast()).source(db.upcast())
}
}
impl HasSource for TypeAlias {
type Ast = ast::TypeAlias;
fn source(self, db: &dyn HirDatabase) -> InFile<ast::TypeAlias> {
self.id.lookup(db.upcast()).source(db.upcast())
}
}
impl HasSource for MacroDef {
type Ast = ast::MacroCall;
fn source(self, db: &dyn HirDatabase) -> InFile<ast::MacroCall> {
InFile {
file_id: self.id.ast_id.expect("MacroDef without ast_id").file_id,
value: self.id.ast_id.expect("MacroDef without ast_id").to_node(db.upcast()),
}
}
}
impl HasSource for ImplDef {
type Ast = ast::Impl;
fn source(self, db: &dyn HirDatabase) -> InFile<ast::Impl> {
self.id.lookup(db.upcast()).source(db.upcast())
}
}
impl HasSource for TypeParam {
type Ast = Either<ast::Trait, ast::TypeParam>;
fn source(self, db: &dyn HirDatabase) -> InFile<Self::Ast> {
let child_source = self.id.parent.child_source(db.upcast());
child_source.map(|it| it[self.id.local_id].clone())
}
}

69
crates/hir/src/lib.rs Normal file
View file

@ -0,0 +1,69 @@
//! HIR (previously known as descriptors) provides a high-level object oriented
//! access to Rust code.
//!
//! The principal difference between HIR and syntax trees is that HIR is bound
//! to a particular crate instance. That is, it has cfg flags and features
//! applied. So, the relation between syntax and HIR is many-to-one.
//!
//! HIR is the public API of the all of the compiler logic above syntax trees.
//! It is written in "OO" style. Each type is self contained (as in, it knows it's
//! parents and full context). It should be "clean code".
//!
//! `hir_*` crates are the implementation of the compiler logic.
//! They are written in "ECS" style, with relatively little abstractions.
//! Many types are not self-contained, and explicitly use local indexes, arenas, etc.
//!
//! `hir` is what insulates the "we don't know how to actually write an incremental compiler"
//! from the ide with completions, hovers, etc. It is a (soft, internal) boundary:
//! https://www.tedinski.com/2018/02/06/system-boundaries.html.
#![recursion_limit = "512"]
mod semantics;
pub mod db;
mod source_analyzer;
pub mod diagnostics;
mod from_id;
mod code_model;
mod link_rewrite;
mod has_source;
pub use crate::{
code_model::{
Access, Adt, AsAssocItem, AssocItem, AssocItemContainer, AttrDef, Callable, CallableKind,
Const, Crate, CrateDependency, DefWithBody, Docs, Enum, EnumVariant, Field, FieldSource,
Function, GenericDef, HasAttrs, HasVisibility, ImplDef, Local, MacroDef, Module, ModuleDef,
ScopeDef, Static, Struct, Trait, Type, TypeAlias, TypeParam, Union, VariantDef, Visibility,
},
has_source::HasSource,
link_rewrite::resolve_doc_link,
semantics::{original_range, PathResolution, Semantics, SemanticsScope},
};
pub use hir_def::{
adt::StructKind,
attr::Attrs,
body::scope::ExprScopes,
builtin_type::BuiltinType,
docs::Documentation,
item_scope::ItemInNs,
nameres::ModuleSource,
path::ModPath,
type_ref::{Mutability, TypeRef},
};
pub use hir_expand::{
name::AsName, name::Name, HirFileId, InFile, MacroCallId, MacroCallLoc,
/* FIXME */ MacroDefId, MacroFile, Origin,
};
pub use hir_ty::display::HirDisplay;
// These are negative re-exports: pub using these names is forbidden, they
// should remain private to hir internals.
#[allow(unused)]
use {
hir_def::path::{Path, PathKind},
hir_expand::hygiene::Hygiene,
};

View file

@ -0,0 +1,226 @@
//! Resolves and rewrites links in markdown documentation for hovers/completion windows.
use std::iter::once;
use itertools::Itertools;
use url::Url;
use crate::{db::HirDatabase, Adt, AsName, Crate, Hygiene, ItemInNs, ModPath, ModuleDef};
use hir_def::{db::DefDatabase, resolver::Resolver};
use syntax::ast::Path;
pub fn resolve_doc_link<T: Resolvable + Clone, D: DefDatabase + HirDatabase>(
db: &D,
definition: &T,
link_text: &str,
link_target: &str,
) -> Option<(String, String)> {
try_resolve_intra(db, definition, link_text, &link_target).or_else(|| {
if let Some(definition) = definition.clone().try_into_module_def() {
try_resolve_path(db, &definition, &link_target)
.map(|target| (target, link_text.to_string()))
} else {
None
}
})
}
/// Try to resolve path to local documentation via intra-doc-links (i.e. `super::gateway::Shard`).
///
/// See [RFC1946](https://github.com/rust-lang/rfcs/blob/master/text/1946-intra-rustdoc-links.md).
fn try_resolve_intra<T: Resolvable, D: DefDatabase + HirDatabase>(
db: &D,
definition: &T,
link_text: &str,
link_target: &str,
) -> Option<(String, String)> {
// Set link_target for implied shortlinks
let link_target =
if link_target.is_empty() { link_text.trim_matches('`') } else { link_target };
// Namespace disambiguation
let namespace = Namespace::from_intra_spec(link_target);
// Strip prefixes/suffixes
let link_target = strip_prefixes_suffixes(link_target);
// Parse link as a module path
let path = Path::parse(link_target).ok()?;
let modpath = ModPath::from_src(path, &Hygiene::new_unhygienic()).unwrap();
// Resolve it relative to symbol's location (according to the RFC this should consider small scopes)
let resolver = definition.resolver(db)?;
let resolved = resolver.resolve_module_path_in_items(db, &modpath);
let (defid, namespace) = match namespace {
// FIXME: .or(resolved.macros)
None => resolved
.types
.map(|t| (t.0, Namespace::Types))
.or(resolved.values.map(|t| (t.0, Namespace::Values)))?,
Some(ns @ Namespace::Types) => (resolved.types?.0, ns),
Some(ns @ Namespace::Values) => (resolved.values?.0, ns),
// FIXME:
Some(Namespace::Macros) => None?,
};
// Get the filepath of the final symbol
let def: ModuleDef = defid.into();
let module = def.module(db)?;
let krate = module.krate();
let ns = match namespace {
Namespace::Types => ItemInNs::Types(defid),
Namespace::Values => ItemInNs::Values(defid),
// FIXME:
Namespace::Macros => None?,
};
let import_map = db.import_map(krate.into());
let path = import_map.path_of(ns)?;
Some((
get_doc_url(db, &krate)?
.join(&format!("{}/", krate.display_name(db)?))
.ok()?
.join(&path.segments.iter().map(|name| name.to_string()).join("/"))
.ok()?
.join(&get_symbol_filename(db, &def)?)
.ok()?
.into_string(),
strip_prefixes_suffixes(link_text).to_string(),
))
}
/// Try to resolve path to local documentation via path-based links (i.e. `../gateway/struct.Shard.html`).
fn try_resolve_path(db: &dyn HirDatabase, moddef: &ModuleDef, link_target: &str) -> Option<String> {
if !link_target.contains("#") && !link_target.contains(".html") {
return None;
}
let ns = ItemInNs::Types(moddef.clone().into());
let module = moddef.module(db)?;
let krate = module.krate();
let import_map = db.import_map(krate.into());
let base = once(format!("{}", krate.display_name(db)?))
.chain(import_map.path_of(ns)?.segments.iter().map(|name| format!("{}", name)))
.join("/");
get_doc_url(db, &krate)
.and_then(|url| url.join(&base).ok())
.and_then(|url| {
get_symbol_filename(db, moddef).as_deref().map(|f| url.join(f).ok()).flatten()
})
.and_then(|url| url.join(link_target).ok())
.map(|url| url.into_string())
}
// Strip prefixes, suffixes, and inline code marks from the given string.
fn strip_prefixes_suffixes(mut s: &str) -> &str {
s = s.trim_matches('`');
[
(TYPES.0.iter(), TYPES.1.iter()),
(VALUES.0.iter(), VALUES.1.iter()),
(MACROS.0.iter(), MACROS.1.iter()),
]
.iter()
.for_each(|(prefixes, suffixes)| {
prefixes.clone().for_each(|prefix| s = s.trim_start_matches(*prefix));
suffixes.clone().for_each(|suffix| s = s.trim_end_matches(*suffix));
});
let s = s.trim_start_matches("@").trim();
s
}
fn get_doc_url(db: &dyn HirDatabase, krate: &Crate) -> Option<Url> {
krate
.get_doc_url(db)
.or_else(||
// Fallback to docs.rs
// FIXME: Specify an exact version here. This may be difficult, as multiple versions of the same crate could exist.
Some(format!("https://docs.rs/{}/*/", krate.display_name(db)?)))
.and_then(|s| Url::parse(&s).ok())
}
/// Get the filename and extension generated for a symbol by rustdoc.
///
/// Example: `struct.Shard.html`
fn get_symbol_filename(db: &dyn HirDatabase, definition: &ModuleDef) -> Option<String> {
Some(match definition {
ModuleDef::Adt(adt) => match adt {
Adt::Struct(s) => format!("struct.{}.html", s.name(db)),
Adt::Enum(e) => format!("enum.{}.html", e.name(db)),
Adt::Union(u) => format!("union.{}.html", u.name(db)),
},
ModuleDef::Module(_) => "index.html".to_string(),
ModuleDef::Trait(t) => format!("trait.{}.html", t.name(db)),
ModuleDef::TypeAlias(t) => format!("type.{}.html", t.name(db)),
ModuleDef::BuiltinType(t) => format!("primitive.{}.html", t.as_name()),
ModuleDef::Function(f) => format!("fn.{}.html", f.name(db)),
ModuleDef::EnumVariant(ev) => {
format!("enum.{}.html#variant.{}", ev.parent_enum(db).name(db), ev.name(db))
}
ModuleDef::Const(c) => format!("const.{}.html", c.name(db)?),
ModuleDef::Static(s) => format!("static.{}.html", s.name(db)?),
})
}
#[derive(PartialEq, Eq, Hash, Copy, Clone, Debug)]
enum Namespace {
Types,
Values,
Macros,
}
static TYPES: ([&str; 7], [&str; 0]) =
(["type", "struct", "enum", "mod", "trait", "union", "module"], []);
static VALUES: ([&str; 8], [&str; 1]) =
(["value", "function", "fn", "method", "const", "static", "mod", "module"], ["()"]);
static MACROS: ([&str; 1], [&str; 1]) = (["macro"], ["!"]);
impl Namespace {
/// Extract the specified namespace from an intra-doc-link if one exists.
///
/// # Examples
///
/// * `struct MyStruct` -> `Namespace::Types`
/// * `panic!` -> `Namespace::Macros`
/// * `fn@from_intra_spec` -> `Namespace::Values`
fn from_intra_spec(s: &str) -> Option<Self> {
[
(Namespace::Types, (TYPES.0.iter(), TYPES.1.iter())),
(Namespace::Values, (VALUES.0.iter(), VALUES.1.iter())),
(Namespace::Macros, (MACROS.0.iter(), MACROS.1.iter())),
]
.iter()
.filter(|(_ns, (prefixes, suffixes))| {
prefixes
.clone()
.map(|prefix| {
s.starts_with(*prefix)
&& s.chars()
.nth(prefix.len() + 1)
.map(|c| c == '@' || c == ' ')
.unwrap_or(false)
})
.any(|cond| cond)
|| suffixes
.clone()
.map(|suffix| {
s.starts_with(*suffix)
&& s.chars()
.nth(suffix.len() + 1)
.map(|c| c == '@' || c == ' ')
.unwrap_or(false)
})
.any(|cond| cond)
})
.map(|(ns, (_, _))| *ns)
.next()
}
}
/// Sealed trait used solely for the generic bound on [`resolve_doc_link`].
pub trait Resolvable {
fn resolver<D: DefDatabase + HirDatabase>(&self, db: &D) -> Option<Resolver>;
fn try_into_module_def(self) -> Option<ModuleDef>;
}

819
crates/hir/src/semantics.rs Normal file
View file

@ -0,0 +1,819 @@
//! See `Semantics`.
mod source_to_def;
use std::{cell::RefCell, fmt, iter::successors};
use base_db::{FileId, FileRange};
use hir_def::{
resolver::{self, HasResolver, Resolver, TypeNs},
AsMacroCall, FunctionId, TraitId, VariantId,
};
use hir_expand::{hygiene::Hygiene, name::AsName, ExpansionInfo};
use hir_ty::associated_type_shorthand_candidates;
use itertools::Itertools;
use rustc_hash::{FxHashMap, FxHashSet};
use syntax::{
algo::{find_node_at_offset, skip_trivia_token},
ast, AstNode, Direction, SyntaxNode, SyntaxToken, TextRange, TextSize,
};
use crate::{
code_model::Access,
db::HirDatabase,
diagnostics::Diagnostic,
semantics::source_to_def::{ChildContainer, SourceToDefCache, SourceToDefCtx},
source_analyzer::{resolve_hir_path, SourceAnalyzer},
AssocItem, Callable, Crate, Field, Function, HirFileId, ImplDef, InFile, Local, MacroDef,
Module, ModuleDef, Name, Origin, Path, ScopeDef, Trait, Type, TypeAlias, TypeParam, VariantDef,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PathResolution {
/// An item
Def(ModuleDef),
/// A local binding (only value namespace)
Local(Local),
/// A generic parameter
TypeParam(TypeParam),
SelfType(ImplDef),
Macro(MacroDef),
AssocItem(AssocItem),
}
impl PathResolution {
fn in_type_ns(&self) -> Option<TypeNs> {
match self {
PathResolution::Def(ModuleDef::Adt(adt)) => Some(TypeNs::AdtId((*adt).into())),
PathResolution::Def(ModuleDef::BuiltinType(builtin)) => {
Some(TypeNs::BuiltinType(*builtin))
}
PathResolution::Def(ModuleDef::Const(_))
| PathResolution::Def(ModuleDef::EnumVariant(_))
| PathResolution::Def(ModuleDef::Function(_))
| PathResolution::Def(ModuleDef::Module(_))
| PathResolution::Def(ModuleDef::Static(_))
| PathResolution::Def(ModuleDef::Trait(_)) => None,
PathResolution::Def(ModuleDef::TypeAlias(alias)) => {
Some(TypeNs::TypeAliasId((*alias).into()))
}
PathResolution::Local(_) | PathResolution::Macro(_) => None,
PathResolution::TypeParam(param) => Some(TypeNs::GenericParam((*param).into())),
PathResolution::SelfType(impl_def) => Some(TypeNs::SelfType((*impl_def).into())),
PathResolution::AssocItem(AssocItem::Const(_))
| PathResolution::AssocItem(AssocItem::Function(_)) => None,
PathResolution::AssocItem(AssocItem::TypeAlias(alias)) => {
Some(TypeNs::TypeAliasId((*alias).into()))
}
}
}
/// Returns an iterator over associated types that may be specified after this path (using
/// `Ty::Assoc` syntax).
pub fn assoc_type_shorthand_candidates<R>(
&self,
db: &dyn HirDatabase,
mut cb: impl FnMut(TypeAlias) -> Option<R>,
) -> Option<R> {
associated_type_shorthand_candidates(db, self.in_type_ns()?, |_, _, id| cb(id.into()))
}
}
/// Primary API to get semantic information, like types, from syntax trees.
pub struct Semantics<'db, DB> {
pub db: &'db DB,
imp: SemanticsImpl<'db>,
}
pub struct SemanticsImpl<'db> {
pub db: &'db dyn HirDatabase,
s2d_cache: RefCell<SourceToDefCache>,
expansion_info_cache: RefCell<FxHashMap<HirFileId, Option<ExpansionInfo>>>,
cache: RefCell<FxHashMap<SyntaxNode, HirFileId>>,
}
impl<DB> fmt::Debug for Semantics<'_, DB> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Semantics {{ ... }}")
}
}
impl<'db, DB: HirDatabase> Semantics<'db, DB> {
pub fn new(db: &DB) -> Semantics<DB> {
let impl_ = SemanticsImpl::new(db);
Semantics { db, imp: impl_ }
}
pub fn parse(&self, file_id: FileId) -> ast::SourceFile {
self.imp.parse(file_id)
}
pub fn expand(&self, macro_call: &ast::MacroCall) -> Option<SyntaxNode> {
self.imp.expand(macro_call)
}
pub fn speculative_expand(
&self,
actual_macro_call: &ast::MacroCall,
hypothetical_args: &ast::TokenTree,
token_to_map: SyntaxToken,
) -> Option<(SyntaxNode, SyntaxToken)> {
self.imp.speculative_expand(actual_macro_call, hypothetical_args, token_to_map)
}
pub fn descend_into_macros(&self, token: SyntaxToken) -> SyntaxToken {
self.imp.descend_into_macros(token)
}
pub fn descend_node_at_offset<N: ast::AstNode>(
&self,
node: &SyntaxNode,
offset: TextSize,
) -> Option<N> {
self.imp.descend_node_at_offset(node, offset).find_map(N::cast)
}
pub fn original_range(&self, node: &SyntaxNode) -> FileRange {
self.imp.original_range(node)
}
pub fn diagnostics_display_range(&self, diagnostics: &dyn Diagnostic) -> FileRange {
self.imp.diagnostics_display_range(diagnostics)
}
pub fn ancestors_with_macros(&self, node: SyntaxNode) -> impl Iterator<Item = SyntaxNode> + '_ {
self.imp.ancestors_with_macros(node)
}
pub fn ancestors_at_offset_with_macros(
&self,
node: &SyntaxNode,
offset: TextSize,
) -> impl Iterator<Item = SyntaxNode> + '_ {
self.imp.ancestors_at_offset_with_macros(node, offset)
}
/// Find a AstNode by offset inside SyntaxNode, if it is inside *Macrofile*,
/// search up until it is of the target AstNode type
pub fn find_node_at_offset_with_macros<N: AstNode>(
&self,
node: &SyntaxNode,
offset: TextSize,
) -> Option<N> {
self.imp.ancestors_at_offset_with_macros(node, offset).find_map(N::cast)
}
/// Find a AstNode by offset inside SyntaxNode, if it is inside *MacroCall*,
/// descend it and find again
pub fn find_node_at_offset_with_descend<N: AstNode>(
&self,
node: &SyntaxNode,
offset: TextSize,
) -> Option<N> {
if let Some(it) = find_node_at_offset(&node, offset) {
return Some(it);
}
self.imp.descend_node_at_offset(node, offset).find_map(N::cast)
}
pub fn type_of_expr(&self, expr: &ast::Expr) -> Option<Type> {
self.imp.type_of_expr(expr)
}
pub fn type_of_pat(&self, pat: &ast::Pat) -> Option<Type> {
self.imp.type_of_pat(pat)
}
pub fn type_of_self(&self, param: &ast::SelfParam) -> Option<Type> {
self.imp.type_of_self(param)
}
pub fn resolve_method_call(&self, call: &ast::MethodCallExpr) -> Option<Function> {
self.imp.resolve_method_call(call).map(Function::from)
}
pub fn resolve_method_call_as_callable(&self, call: &ast::MethodCallExpr) -> Option<Callable> {
self.imp.resolve_method_call_as_callable(call)
}
pub fn resolve_field(&self, field: &ast::FieldExpr) -> Option<Field> {
self.imp.resolve_field(field)
}
pub fn resolve_record_field(
&self,
field: &ast::RecordExprField,
) -> Option<(Field, Option<Local>)> {
self.imp.resolve_record_field(field)
}
pub fn resolve_record_field_pat(&self, field: &ast::RecordPatField) -> Option<Field> {
self.imp.resolve_record_field_pat(field)
}
pub fn resolve_macro_call(&self, macro_call: &ast::MacroCall) -> Option<MacroDef> {
self.imp.resolve_macro_call(macro_call)
}
pub fn resolve_path(&self, path: &ast::Path) -> Option<PathResolution> {
self.imp.resolve_path(path)
}
pub fn resolve_extern_crate(&self, extern_crate: &ast::ExternCrate) -> Option<Crate> {
self.imp.resolve_extern_crate(extern_crate)
}
pub fn resolve_variant(&self, record_lit: ast::RecordExpr) -> Option<VariantDef> {
self.imp.resolve_variant(record_lit).map(VariantDef::from)
}
pub fn resolve_bind_pat_to_const(&self, pat: &ast::IdentPat) -> Option<ModuleDef> {
self.imp.resolve_bind_pat_to_const(pat)
}
// FIXME: use this instead?
// pub fn resolve_name_ref(&self, name_ref: &ast::NameRef) -> Option<???>;
pub fn record_literal_missing_fields(&self, literal: &ast::RecordExpr) -> Vec<(Field, Type)> {
self.imp.record_literal_missing_fields(literal)
}
pub fn record_pattern_missing_fields(&self, pattern: &ast::RecordPat) -> Vec<(Field, Type)> {
self.imp.record_pattern_missing_fields(pattern)
}
pub fn to_def<T: ToDef>(&self, src: &T) -> Option<T::Def> {
let src = self.imp.find_file(src.syntax().clone()).with_value(src).cloned();
T::to_def(&self.imp, src)
}
pub fn to_module_def(&self, file: FileId) -> Option<Module> {
self.imp.to_module_def(file)
}
pub fn scope(&self, node: &SyntaxNode) -> SemanticsScope<'db> {
self.imp.scope(node)
}
pub fn scope_at_offset(&self, node: &SyntaxNode, offset: TextSize) -> SemanticsScope<'db> {
self.imp.scope_at_offset(node, offset)
}
pub fn scope_for_def(&self, def: Trait) -> SemanticsScope<'db> {
self.imp.scope_for_def(def)
}
pub fn assert_contains_node(&self, node: &SyntaxNode) {
self.imp.assert_contains_node(node)
}
pub fn is_unsafe_method_call(&self, method_call_expr: &ast::MethodCallExpr) -> bool {
self.imp.is_unsafe_method_call(method_call_expr)
}
pub fn is_unsafe_ref_expr(&self, ref_expr: &ast::RefExpr) -> bool {
self.imp.is_unsafe_ref_expr(ref_expr)
}
pub fn is_unsafe_ident_pat(&self, ident_pat: &ast::IdentPat) -> bool {
self.imp.is_unsafe_ident_pat(ident_pat)
}
}
impl<'db> SemanticsImpl<'db> {
fn new(db: &'db dyn HirDatabase) -> Self {
SemanticsImpl {
db,
s2d_cache: Default::default(),
cache: Default::default(),
expansion_info_cache: Default::default(),
}
}
fn parse(&self, file_id: FileId) -> ast::SourceFile {
let tree = self.db.parse(file_id).tree();
self.cache(tree.syntax().clone(), file_id.into());
tree
}
fn expand(&self, macro_call: &ast::MacroCall) -> Option<SyntaxNode> {
let macro_call = self.find_file(macro_call.syntax().clone()).with_value(macro_call);
let sa = self.analyze2(macro_call.map(|it| it.syntax()), None);
let file_id = sa.expand(self.db, macro_call)?;
let node = self.db.parse_or_expand(file_id)?;
self.cache(node.clone(), file_id);
Some(node)
}
fn speculative_expand(
&self,
actual_macro_call: &ast::MacroCall,
hypothetical_args: &ast::TokenTree,
token_to_map: SyntaxToken,
) -> Option<(SyntaxNode, SyntaxToken)> {
let macro_call =
self.find_file(actual_macro_call.syntax().clone()).with_value(actual_macro_call);
let sa = self.analyze2(macro_call.map(|it| it.syntax()), None);
let krate = sa.resolver.krate()?;
let macro_call_id = macro_call.as_call_id(self.db.upcast(), krate, |path| {
sa.resolver.resolve_path_as_macro(self.db.upcast(), &path)
})?;
hir_expand::db::expand_hypothetical(
self.db.upcast(),
macro_call_id,
hypothetical_args,
token_to_map,
)
}
fn descend_into_macros(&self, token: SyntaxToken) -> SyntaxToken {
let _p = profile::span("descend_into_macros");
let parent = token.parent();
let parent = self.find_file(parent);
let sa = self.analyze2(parent.as_ref(), None);
let token = successors(Some(parent.with_value(token)), |token| {
self.db.check_canceled();
let macro_call = token.value.ancestors().find_map(ast::MacroCall::cast)?;
let tt = macro_call.token_tree()?;
if !tt.syntax().text_range().contains_range(token.value.text_range()) {
return None;
}
let file_id = sa.expand(self.db, token.with_value(&macro_call))?;
let token = self
.expansion_info_cache
.borrow_mut()
.entry(file_id)
.or_insert_with(|| file_id.expansion_info(self.db.upcast()))
.as_ref()?
.map_token_down(token.as_ref())?;
self.cache(find_root(&token.value.parent()), token.file_id);
Some(token)
})
.last()
.unwrap();
token.value
}
fn descend_node_at_offset(
&self,
node: &SyntaxNode,
offset: TextSize,
) -> impl Iterator<Item = SyntaxNode> + '_ {
// Handle macro token cases
node.token_at_offset(offset)
.map(|token| self.descend_into_macros(token))
.map(|it| self.ancestors_with_macros(it.parent()))
.flatten()
}
fn original_range(&self, node: &SyntaxNode) -> FileRange {
let node = self.find_file(node.clone());
original_range(self.db, node.as_ref())
}
fn diagnostics_display_range(&self, diagnostics: &dyn Diagnostic) -> FileRange {
let src = diagnostics.display_source();
let root = self.db.parse_or_expand(src.file_id).unwrap();
let node = src.value.to_node(&root);
self.cache(root, src.file_id);
original_range(self.db, src.with_value(&node))
}
fn ancestors_with_macros(&self, node: SyntaxNode) -> impl Iterator<Item = SyntaxNode> + '_ {
let node = self.find_file(node);
node.ancestors_with_macros(self.db.upcast()).map(|it| it.value)
}
fn ancestors_at_offset_with_macros(
&self,
node: &SyntaxNode,
offset: TextSize,
) -> impl Iterator<Item = SyntaxNode> + '_ {
node.token_at_offset(offset)
.map(|token| self.ancestors_with_macros(token.parent()))
.kmerge_by(|node1, node2| node1.text_range().len() < node2.text_range().len())
}
fn type_of_expr(&self, expr: &ast::Expr) -> Option<Type> {
self.analyze(expr.syntax()).type_of_expr(self.db, &expr)
}
fn type_of_pat(&self, pat: &ast::Pat) -> Option<Type> {
self.analyze(pat.syntax()).type_of_pat(self.db, &pat)
}
fn type_of_self(&self, param: &ast::SelfParam) -> Option<Type> {
self.analyze(param.syntax()).type_of_self(self.db, &param)
}
fn resolve_method_call(&self, call: &ast::MethodCallExpr) -> Option<FunctionId> {
self.analyze(call.syntax()).resolve_method_call(self.db, call)
}
fn resolve_method_call_as_callable(&self, call: &ast::MethodCallExpr) -> Option<Callable> {
// FIXME: this erases Substs
let func = self.resolve_method_call(call)?;
let ty = self.db.value_ty(func.into());
let resolver = self.analyze(call.syntax()).resolver;
let ty = Type::new_with_resolver(self.db, &resolver, ty.value)?;
let mut res = ty.as_callable(self.db)?;
res.is_bound_method = true;
Some(res)
}
fn resolve_field(&self, field: &ast::FieldExpr) -> Option<Field> {
self.analyze(field.syntax()).resolve_field(self.db, field)
}
fn resolve_record_field(&self, field: &ast::RecordExprField) -> Option<(Field, Option<Local>)> {
self.analyze(field.syntax()).resolve_record_field(self.db, field)
}
fn resolve_record_field_pat(&self, field: &ast::RecordPatField) -> Option<Field> {
self.analyze(field.syntax()).resolve_record_field_pat(self.db, field)
}
fn resolve_macro_call(&self, macro_call: &ast::MacroCall) -> Option<MacroDef> {
let sa = self.analyze(macro_call.syntax());
let macro_call = self.find_file(macro_call.syntax().clone()).with_value(macro_call);
sa.resolve_macro_call(self.db, macro_call)
}
fn resolve_path(&self, path: &ast::Path) -> Option<PathResolution> {
self.analyze(path.syntax()).resolve_path(self.db, path)
}
fn resolve_extern_crate(&self, extern_crate: &ast::ExternCrate) -> Option<Crate> {
let krate = self.scope(extern_crate.syntax()).krate()?;
krate.dependencies(self.db).into_iter().find_map(|dep| {
if dep.name == extern_crate.name_ref()?.as_name() {
Some(dep.krate)
} else {
None
}
})
}
fn resolve_variant(&self, record_lit: ast::RecordExpr) -> Option<VariantId> {
self.analyze(record_lit.syntax()).resolve_variant(self.db, record_lit)
}
fn resolve_bind_pat_to_const(&self, pat: &ast::IdentPat) -> Option<ModuleDef> {
self.analyze(pat.syntax()).resolve_bind_pat_to_const(self.db, pat)
}
fn record_literal_missing_fields(&self, literal: &ast::RecordExpr) -> Vec<(Field, Type)> {
self.analyze(literal.syntax())
.record_literal_missing_fields(self.db, literal)
.unwrap_or_default()
}
fn record_pattern_missing_fields(&self, pattern: &ast::RecordPat) -> Vec<(Field, Type)> {
self.analyze(pattern.syntax())
.record_pattern_missing_fields(self.db, pattern)
.unwrap_or_default()
}
fn with_ctx<F: FnOnce(&mut SourceToDefCtx) -> T, T>(&self, f: F) -> T {
let mut cache = self.s2d_cache.borrow_mut();
let mut ctx = SourceToDefCtx { db: self.db, cache: &mut *cache };
f(&mut ctx)
}
fn to_module_def(&self, file: FileId) -> Option<Module> {
self.with_ctx(|ctx| ctx.file_to_def(file)).map(Module::from)
}
fn scope(&self, node: &SyntaxNode) -> SemanticsScope<'db> {
let node = self.find_file(node.clone());
let resolver = self.analyze2(node.as_ref(), None).resolver;
SemanticsScope { db: self.db, file_id: node.file_id, resolver }
}
fn scope_at_offset(&self, node: &SyntaxNode, offset: TextSize) -> SemanticsScope<'db> {
let node = self.find_file(node.clone());
let resolver = self.analyze2(node.as_ref(), Some(offset)).resolver;
SemanticsScope { db: self.db, file_id: node.file_id, resolver }
}
fn scope_for_def(&self, def: Trait) -> SemanticsScope<'db> {
let file_id = self.db.lookup_intern_trait(def.id).id.file_id;
let resolver = def.id.resolver(self.db.upcast());
SemanticsScope { db: self.db, file_id, resolver }
}
fn analyze(&self, node: &SyntaxNode) -> SourceAnalyzer {
let src = self.find_file(node.clone());
self.analyze2(src.as_ref(), None)
}
fn analyze2(&self, src: InFile<&SyntaxNode>, offset: Option<TextSize>) -> SourceAnalyzer {
let _p = profile::span("Semantics::analyze2");
let container = match self.with_ctx(|ctx| ctx.find_container(src)) {
Some(it) => it,
None => return SourceAnalyzer::new_for_resolver(Resolver::default(), src),
};
let resolver = match container {
ChildContainer::DefWithBodyId(def) => {
return SourceAnalyzer::new_for_body(self.db, def, src, offset)
}
ChildContainer::TraitId(it) => it.resolver(self.db.upcast()),
ChildContainer::ImplId(it) => it.resolver(self.db.upcast()),
ChildContainer::ModuleId(it) => it.resolver(self.db.upcast()),
ChildContainer::EnumId(it) => it.resolver(self.db.upcast()),
ChildContainer::VariantId(it) => it.resolver(self.db.upcast()),
ChildContainer::TypeAliasId(it) => it.resolver(self.db.upcast()),
ChildContainer::GenericDefId(it) => it.resolver(self.db.upcast()),
};
SourceAnalyzer::new_for_resolver(resolver, src)
}
fn cache(&self, root_node: SyntaxNode, file_id: HirFileId) {
assert!(root_node.parent().is_none());
let mut cache = self.cache.borrow_mut();
let prev = cache.insert(root_node, file_id);
assert!(prev == None || prev == Some(file_id))
}
fn assert_contains_node(&self, node: &SyntaxNode) {
self.find_file(node.clone());
}
fn lookup(&self, root_node: &SyntaxNode) -> Option<HirFileId> {
let cache = self.cache.borrow();
cache.get(root_node).copied()
}
fn find_file(&self, node: SyntaxNode) -> InFile<SyntaxNode> {
let root_node = find_root(&node);
let file_id = self.lookup(&root_node).unwrap_or_else(|| {
panic!(
"\n\nFailed to lookup {:?} in this Semantics.\n\
Make sure to use only query nodes, derived from this instance of Semantics.\n\
root node: {:?}\n\
known nodes: {}\n\n",
node,
root_node,
self.cache
.borrow()
.keys()
.map(|it| format!("{:?}", it))
.collect::<Vec<_>>()
.join(", ")
)
});
InFile::new(file_id, node)
}
fn is_unsafe_method_call(&self, method_call_expr: &ast::MethodCallExpr) -> bool {
method_call_expr
.receiver()
.and_then(|expr| {
let field_expr = match expr {
ast::Expr::FieldExpr(field_expr) => field_expr,
_ => return None,
};
let ty = self.type_of_expr(&field_expr.expr()?)?;
if !ty.is_packed(self.db) {
return None;
}
let func = self.resolve_method_call(&method_call_expr).map(Function::from)?;
let res = match func.self_param(self.db)?.access(self.db) {
Access::Shared | Access::Exclusive => true,
Access::Owned => false,
};
Some(res)
})
.unwrap_or(false)
}
fn is_unsafe_ref_expr(&self, ref_expr: &ast::RefExpr) -> bool {
ref_expr
.expr()
.and_then(|expr| {
let field_expr = match expr {
ast::Expr::FieldExpr(field_expr) => field_expr,
_ => return None,
};
let expr = field_expr.expr()?;
self.type_of_expr(&expr)
})
// Binding a reference to a packed type is possibly unsafe.
.map(|ty| ty.is_packed(self.db))
.unwrap_or(false)
// FIXME This needs layout computation to be correct. It will highlight
// more than it should with the current implementation.
}
fn is_unsafe_ident_pat(&self, ident_pat: &ast::IdentPat) -> bool {
if !ident_pat.ref_token().is_some() {
return false;
}
ident_pat
.syntax()
.parent()
.and_then(|parent| {
// `IdentPat` can live under `RecordPat` directly under `RecordPatField` or
// `RecordPatFieldList`. `RecordPatField` also lives under `RecordPatFieldList`,
// so this tries to lookup the `IdentPat` anywhere along that structure to the
// `RecordPat` so we can get the containing type.
let record_pat = ast::RecordPatField::cast(parent.clone())
.and_then(|record_pat| record_pat.syntax().parent())
.or_else(|| Some(parent.clone()))
.and_then(|parent| {
ast::RecordPatFieldList::cast(parent)?
.syntax()
.parent()
.and_then(ast::RecordPat::cast)
});
// If this doesn't match a `RecordPat`, fallback to a `LetStmt` to see if
// this is initialized from a `FieldExpr`.
if let Some(record_pat) = record_pat {
self.type_of_pat(&ast::Pat::RecordPat(record_pat))
} else if let Some(let_stmt) = ast::LetStmt::cast(parent) {
let field_expr = match let_stmt.initializer()? {
ast::Expr::FieldExpr(field_expr) => field_expr,
_ => return None,
};
self.type_of_expr(&field_expr.expr()?)
} else {
None
}
})
// Binding a reference to a packed type is possibly unsafe.
.map(|ty| ty.is_packed(self.db))
.unwrap_or(false)
}
}
pub trait ToDef: AstNode + Clone {
type Def;
fn to_def(sema: &SemanticsImpl, src: InFile<Self>) -> Option<Self::Def>;
}
macro_rules! to_def_impls {
($(($def:path, $ast:path, $meth:ident)),* ,) => {$(
impl ToDef for $ast {
type Def = $def;
fn to_def(sema: &SemanticsImpl, src: InFile<Self>) -> Option<Self::Def> {
sema.with_ctx(|ctx| ctx.$meth(src)).map(<$def>::from)
}
}
)*}
}
to_def_impls![
(crate::Module, ast::Module, module_to_def),
(crate::Struct, ast::Struct, struct_to_def),
(crate::Enum, ast::Enum, enum_to_def),
(crate::Union, ast::Union, union_to_def),
(crate::Trait, ast::Trait, trait_to_def),
(crate::ImplDef, ast::Impl, impl_to_def),
(crate::TypeAlias, ast::TypeAlias, type_alias_to_def),
(crate::Const, ast::Const, const_to_def),
(crate::Static, ast::Static, static_to_def),
(crate::Function, ast::Fn, fn_to_def),
(crate::Field, ast::RecordField, record_field_to_def),
(crate::Field, ast::TupleField, tuple_field_to_def),
(crate::EnumVariant, ast::Variant, enum_variant_to_def),
(crate::TypeParam, ast::TypeParam, type_param_to_def),
(crate::MacroDef, ast::MacroCall, macro_call_to_def), // this one is dubious, not all calls are macros
(crate::Local, ast::IdentPat, bind_pat_to_def),
];
fn find_root(node: &SyntaxNode) -> SyntaxNode {
node.ancestors().last().unwrap()
}
#[derive(Debug)]
pub struct SemanticsScope<'a> {
pub db: &'a dyn HirDatabase,
file_id: HirFileId,
resolver: Resolver,
}
impl<'a> SemanticsScope<'a> {
pub fn module(&self) -> Option<Module> {
Some(Module { id: self.resolver.module()? })
}
pub fn krate(&self) -> Option<Crate> {
Some(Crate { id: self.resolver.krate()? })
}
/// Note: `FxHashSet<TraitId>` should be treated as an opaque type, passed into `Type
// FIXME: rename to visible_traits to not repeat scope?
pub fn traits_in_scope(&self) -> FxHashSet<TraitId> {
let resolver = &self.resolver;
resolver.traits_in_scope(self.db.upcast())
}
pub fn process_all_names(&self, f: &mut dyn FnMut(Name, ScopeDef)) {
let resolver = &self.resolver;
resolver.process_all_names(self.db.upcast(), &mut |name, def| {
let def = match def {
resolver::ScopeDef::PerNs(it) => {
let items = ScopeDef::all_items(it);
for item in items {
f(name.clone(), item);
}
return;
}
resolver::ScopeDef::ImplSelfType(it) => ScopeDef::ImplSelfType(it.into()),
resolver::ScopeDef::AdtSelfType(it) => ScopeDef::AdtSelfType(it.into()),
resolver::ScopeDef::GenericParam(id) => ScopeDef::GenericParam(TypeParam { id }),
resolver::ScopeDef::Local(pat_id) => {
let parent = resolver.body_owner().unwrap().into();
ScopeDef::Local(Local { parent, pat_id })
}
};
f(name, def)
})
}
/// Resolve a path as-if it was written at the given scope. This is
/// necessary a heuristic, as it doesn't take hygiene into account.
pub fn speculative_resolve(&self, path: &ast::Path) -> Option<PathResolution> {
let hygiene = Hygiene::new(self.db.upcast(), self.file_id);
let path = Path::from_src(path.clone(), &hygiene)?;
resolve_hir_path(self.db, &self.resolver, &path)
}
}
// FIXME: Change `HasSource` trait to work with `Semantics` and remove this?
pub fn original_range(db: &dyn HirDatabase, node: InFile<&SyntaxNode>) -> FileRange {
if let Some(range) = original_range_opt(db, node) {
let original_file = range.file_id.original_file(db.upcast());
if range.file_id == original_file.into() {
return FileRange { file_id: original_file, range: range.value };
}
log::error!("Fail to mapping up more for {:?}", range);
return FileRange { file_id: range.file_id.original_file(db.upcast()), range: range.value };
}
// Fall back to whole macro call
if let Some(expansion) = node.file_id.expansion_info(db.upcast()) {
if let Some(call_node) = expansion.call_node() {
return FileRange {
file_id: call_node.file_id.original_file(db.upcast()),
range: call_node.value.text_range(),
};
}
}
FileRange { file_id: node.file_id.original_file(db.upcast()), range: node.value.text_range() }
}
fn original_range_opt(
db: &dyn HirDatabase,
node: InFile<&SyntaxNode>,
) -> Option<InFile<TextRange>> {
let expansion = node.file_id.expansion_info(db.upcast())?;
// the input node has only one token ?
let single = skip_trivia_token(node.value.first_token()?, Direction::Next)?
== skip_trivia_token(node.value.last_token()?, Direction::Prev)?;
Some(node.value.descendants().find_map(|it| {
let first = skip_trivia_token(it.first_token()?, Direction::Next)?;
let first = ascend_call_token(db, &expansion, node.with_value(first))?;
let last = skip_trivia_token(it.last_token()?, Direction::Prev)?;
let last = ascend_call_token(db, &expansion, node.with_value(last))?;
if (!single && first == last) || (first.file_id != last.file_id) {
return None;
}
Some(first.with_value(first.value.text_range().cover(last.value.text_range())))
})?)
}
fn ascend_call_token(
db: &dyn HirDatabase,
expansion: &ExpansionInfo,
token: InFile<SyntaxToken>,
) -> Option<InFile<SyntaxToken>> {
let (mapped, origin) = expansion.map_token_up(token.as_ref())?;
if origin != Origin::Call {
return None;
}
if let Some(info) = mapped.file_id.expansion_info(db.upcast()) {
return ascend_call_token(db, &info, mapped);
}
Some(mapped)
}

View file

@ -0,0 +1,275 @@
//! Maps *syntax* of various definitions to their semantic ids.
use base_db::FileId;
use hir_def::{
child_by_source::ChildBySource,
dyn_map::DynMap,
expr::PatId,
keys::{self, Key},
ConstId, DefWithBodyId, EnumId, EnumVariantId, FieldId, FunctionId, GenericDefId, ImplId,
ModuleId, StaticId, StructId, TraitId, TypeAliasId, TypeParamId, UnionId, VariantId,
};
use hir_expand::{name::AsName, AstId, MacroDefKind};
use rustc_hash::FxHashMap;
use stdx::impl_from;
use syntax::{
ast::{self, NameOwner},
match_ast, AstNode, SyntaxNode,
};
use crate::{db::HirDatabase, InFile, MacroDefId};
pub(super) type SourceToDefCache = FxHashMap<ChildContainer, DynMap>;
pub(super) struct SourceToDefCtx<'a, 'b> {
pub(super) db: &'b dyn HirDatabase,
pub(super) cache: &'a mut SourceToDefCache,
}
impl SourceToDefCtx<'_, '_> {
pub(super) fn file_to_def(&mut self, file: FileId) -> Option<ModuleId> {
let _p = profile::span("SourceBinder::to_module_def");
let (krate, local_id) = self.db.relevant_crates(file).iter().find_map(|&crate_id| {
let crate_def_map = self.db.crate_def_map(crate_id);
let local_id = crate_def_map.modules_for_file(file).next()?;
Some((crate_id, local_id))
})?;
Some(ModuleId { krate, local_id })
}
pub(super) fn module_to_def(&mut self, src: InFile<ast::Module>) -> Option<ModuleId> {
let _p = profile::span("module_to_def");
let parent_declaration = src
.as_ref()
.map(|it| it.syntax())
.cloned()
.ancestors_with_macros(self.db.upcast())
.skip(1)
.find_map(|it| {
let m = ast::Module::cast(it.value.clone())?;
Some(it.with_value(m))
});
let parent_module = match parent_declaration {
Some(parent_declaration) => self.module_to_def(parent_declaration),
None => {
let file_id = src.file_id.original_file(self.db.upcast());
self.file_to_def(file_id)
}
}?;
let child_name = src.value.name()?.as_name();
let def_map = self.db.crate_def_map(parent_module.krate);
let child_id = *def_map[parent_module.local_id].children.get(&child_name)?;
Some(ModuleId { krate: parent_module.krate, local_id: child_id })
}
pub(super) fn trait_to_def(&mut self, src: InFile<ast::Trait>) -> Option<TraitId> {
self.to_def(src, keys::TRAIT)
}
pub(super) fn impl_to_def(&mut self, src: InFile<ast::Impl>) -> Option<ImplId> {
self.to_def(src, keys::IMPL)
}
pub(super) fn fn_to_def(&mut self, src: InFile<ast::Fn>) -> Option<FunctionId> {
self.to_def(src, keys::FUNCTION)
}
pub(super) fn struct_to_def(&mut self, src: InFile<ast::Struct>) -> Option<StructId> {
self.to_def(src, keys::STRUCT)
}
pub(super) fn enum_to_def(&mut self, src: InFile<ast::Enum>) -> Option<EnumId> {
self.to_def(src, keys::ENUM)
}
pub(super) fn union_to_def(&mut self, src: InFile<ast::Union>) -> Option<UnionId> {
self.to_def(src, keys::UNION)
}
pub(super) fn static_to_def(&mut self, src: InFile<ast::Static>) -> Option<StaticId> {
self.to_def(src, keys::STATIC)
}
pub(super) fn const_to_def(&mut self, src: InFile<ast::Const>) -> Option<ConstId> {
self.to_def(src, keys::CONST)
}
pub(super) fn type_alias_to_def(&mut self, src: InFile<ast::TypeAlias>) -> Option<TypeAliasId> {
self.to_def(src, keys::TYPE_ALIAS)
}
pub(super) fn record_field_to_def(&mut self, src: InFile<ast::RecordField>) -> Option<FieldId> {
self.to_def(src, keys::RECORD_FIELD)
}
pub(super) fn tuple_field_to_def(&mut self, src: InFile<ast::TupleField>) -> Option<FieldId> {
self.to_def(src, keys::TUPLE_FIELD)
}
pub(super) fn enum_variant_to_def(
&mut self,
src: InFile<ast::Variant>,
) -> Option<EnumVariantId> {
self.to_def(src, keys::VARIANT)
}
pub(super) fn bind_pat_to_def(
&mut self,
src: InFile<ast::IdentPat>,
) -> Option<(DefWithBodyId, PatId)> {
let container = self.find_pat_container(src.as_ref().map(|it| it.syntax()))?;
let (_body, source_map) = self.db.body_with_source_map(container);
let src = src.map(ast::Pat::from);
let pat_id = source_map.node_pat(src.as_ref())?;
Some((container, pat_id))
}
fn to_def<Ast: AstNode + 'static, ID: Copy + 'static>(
&mut self,
src: InFile<Ast>,
key: Key<Ast, ID>,
) -> Option<ID> {
let container = self.find_container(src.as_ref().map(|it| it.syntax()))?;
let db = self.db;
let dyn_map =
&*self.cache.entry(container).or_insert_with(|| container.child_by_source(db));
dyn_map[key].get(&src).copied()
}
pub(super) fn type_param_to_def(&mut self, src: InFile<ast::TypeParam>) -> Option<TypeParamId> {
let container: ChildContainer =
self.find_type_param_container(src.as_ref().map(|it| it.syntax()))?.into();
let db = self.db;
let dyn_map =
&*self.cache.entry(container).or_insert_with(|| container.child_by_source(db));
dyn_map[keys::TYPE_PARAM].get(&src).copied()
}
// FIXME: use DynMap as well?
pub(super) fn macro_call_to_def(&mut self, src: InFile<ast::MacroCall>) -> Option<MacroDefId> {
let kind = MacroDefKind::Declarative;
let file_id = src.file_id.original_file(self.db.upcast());
let krate = self.file_to_def(file_id)?.krate;
let file_ast_id = self.db.ast_id_map(src.file_id).ast_id(&src.value);
let ast_id = Some(AstId::new(src.file_id, file_ast_id));
Some(MacroDefId { krate: Some(krate), ast_id, kind, local_inner: false })
}
pub(super) fn find_container(&mut self, src: InFile<&SyntaxNode>) -> Option<ChildContainer> {
for container in src.cloned().ancestors_with_macros(self.db.upcast()).skip(1) {
let res: ChildContainer = match_ast! {
match (container.value) {
ast::Module(it) => {
let def = self.module_to_def(container.with_value(it))?;
def.into()
},
ast::Trait(it) => {
let def = self.trait_to_def(container.with_value(it))?;
def.into()
},
ast::Impl(it) => {
let def = self.impl_to_def(container.with_value(it))?;
def.into()
},
ast::Fn(it) => {
let def = self.fn_to_def(container.with_value(it))?;
DefWithBodyId::from(def).into()
},
ast::Struct(it) => {
let def = self.struct_to_def(container.with_value(it))?;
VariantId::from(def).into()
},
ast::Enum(it) => {
let def = self.enum_to_def(container.with_value(it))?;
def.into()
},
ast::Union(it) => {
let def = self.union_to_def(container.with_value(it))?;
VariantId::from(def).into()
},
ast::Static(it) => {
let def = self.static_to_def(container.with_value(it))?;
DefWithBodyId::from(def).into()
},
ast::Const(it) => {
let def = self.const_to_def(container.with_value(it))?;
DefWithBodyId::from(def).into()
},
ast::TypeAlias(it) => {
let def = self.type_alias_to_def(container.with_value(it))?;
def.into()
},
_ => continue,
}
};
return Some(res);
}
let def = self.file_to_def(src.file_id.original_file(self.db.upcast()))?;
Some(def.into())
}
fn find_type_param_container(&mut self, src: InFile<&SyntaxNode>) -> Option<GenericDefId> {
for container in src.cloned().ancestors_with_macros(self.db.upcast()).skip(1) {
let res: GenericDefId = match_ast! {
match (container.value) {
ast::Fn(it) => self.fn_to_def(container.with_value(it))?.into(),
ast::Struct(it) => self.struct_to_def(container.with_value(it))?.into(),
ast::Enum(it) => self.enum_to_def(container.with_value(it))?.into(),
ast::Trait(it) => self.trait_to_def(container.with_value(it))?.into(),
ast::TypeAlias(it) => self.type_alias_to_def(container.with_value(it))?.into(),
ast::Impl(it) => self.impl_to_def(container.with_value(it))?.into(),
_ => continue,
}
};
return Some(res);
}
None
}
fn find_pat_container(&mut self, src: InFile<&SyntaxNode>) -> Option<DefWithBodyId> {
for container in src.cloned().ancestors_with_macros(self.db.upcast()).skip(1) {
let res: DefWithBodyId = match_ast! {
match (container.value) {
ast::Const(it) => self.const_to_def(container.with_value(it))?.into(),
ast::Static(it) => self.static_to_def(container.with_value(it))?.into(),
ast::Fn(it) => self.fn_to_def(container.with_value(it))?.into(),
_ => continue,
}
};
return Some(res);
}
None
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub(crate) enum ChildContainer {
DefWithBodyId(DefWithBodyId),
ModuleId(ModuleId),
TraitId(TraitId),
ImplId(ImplId),
EnumId(EnumId),
VariantId(VariantId),
TypeAliasId(TypeAliasId),
/// XXX: this might be the same def as, for example an `EnumId`. However,
/// here the children generic parameters, and not, eg enum variants.
GenericDefId(GenericDefId),
}
impl_from! {
DefWithBodyId,
ModuleId,
TraitId,
ImplId,
EnumId,
VariantId,
TypeAliasId,
GenericDefId
for ChildContainer
}
impl ChildContainer {
fn child_by_source(self, db: &dyn HirDatabase) -> DynMap {
let db = db.upcast();
match self {
ChildContainer::DefWithBodyId(it) => it.child_by_source(db),
ChildContainer::ModuleId(it) => it.child_by_source(db),
ChildContainer::TraitId(it) => it.child_by_source(db),
ChildContainer::ImplId(it) => it.child_by_source(db),
ChildContainer::EnumId(it) => it.child_by_source(db),
ChildContainer::VariantId(it) => it.child_by_source(db),
ChildContainer::TypeAliasId(_) => DynMap::default(),
ChildContainer::GenericDefId(it) => it.child_by_source(db),
}
}
}

View file

@ -0,0 +1,534 @@
//! Lookup hir elements using positions in the source code. This is a lossy
//! transformation: in general, a single source might correspond to several
//! modules, functions, etc, due to macros, cfgs and `#[path=]` attributes on
//! modules.
//!
//! So, this modules should not be used during hir construction, it exists
//! purely for "IDE needs".
use std::{iter::once, sync::Arc};
use hir_def::{
body::{
scope::{ExprScopes, ScopeId},
Body, BodySourceMap,
},
expr::{ExprId, Pat, PatId},
path::{ModPath, Path, PathKind},
resolver::{resolver_for_scope, Resolver, TypeNs, ValueNs},
AsMacroCall, DefWithBodyId, FieldId, FunctionId, LocalFieldId, VariantId,
};
use hir_expand::{hygiene::Hygiene, name::AsName, HirFileId, InFile};
use hir_ty::{
diagnostics::{record_literal_missing_fields, record_pattern_missing_fields},
InferenceResult, Substs, Ty,
};
use syntax::{
ast::{self, AstNode},
SyntaxNode, TextRange, TextSize,
};
use crate::{
db::HirDatabase, semantics::PathResolution, Adt, Const, EnumVariant, Field, Function, Local,
MacroDef, ModuleDef, Static, Struct, Trait, Type, TypeAlias, TypeParam,
};
use base_db::CrateId;
/// `SourceAnalyzer` is a convenience wrapper which exposes HIR API in terms of
/// original source files. It should not be used inside the HIR itself.
#[derive(Debug)]
pub(crate) struct SourceAnalyzer {
file_id: HirFileId,
pub(crate) resolver: Resolver,
body: Option<Arc<Body>>,
body_source_map: Option<Arc<BodySourceMap>>,
infer: Option<Arc<InferenceResult>>,
scopes: Option<Arc<ExprScopes>>,
}
impl SourceAnalyzer {
pub(crate) fn new_for_body(
db: &dyn HirDatabase,
def: DefWithBodyId,
node: InFile<&SyntaxNode>,
offset: Option<TextSize>,
) -> SourceAnalyzer {
let (body, source_map) = db.body_with_source_map(def);
let scopes = db.expr_scopes(def);
let scope = match offset {
None => scope_for(&scopes, &source_map, node),
Some(offset) => scope_for_offset(db, &scopes, &source_map, node.with_value(offset)),
};
let resolver = resolver_for_scope(db.upcast(), def, scope);
SourceAnalyzer {
resolver,
body: Some(body),
body_source_map: Some(source_map),
infer: Some(db.infer(def)),
scopes: Some(scopes),
file_id: node.file_id,
}
}
pub(crate) fn new_for_resolver(
resolver: Resolver,
node: InFile<&SyntaxNode>,
) -> SourceAnalyzer {
SourceAnalyzer {
resolver,
body: None,
body_source_map: None,
infer: None,
scopes: None,
file_id: node.file_id,
}
}
fn expr_id(&self, db: &dyn HirDatabase, expr: &ast::Expr) -> Option<ExprId> {
let src = match expr {
ast::Expr::MacroCall(call) => {
self.expand_expr(db, InFile::new(self.file_id, call.clone()))?
}
_ => InFile::new(self.file_id, expr.clone()),
};
let sm = self.body_source_map.as_ref()?;
sm.node_expr(src.as_ref())
}
fn pat_id(&self, pat: &ast::Pat) -> Option<PatId> {
// FIXME: macros, see `expr_id`
let src = InFile { file_id: self.file_id, value: pat };
self.body_source_map.as_ref()?.node_pat(src)
}
fn expand_expr(
&self,
db: &dyn HirDatabase,
expr: InFile<ast::MacroCall>,
) -> Option<InFile<ast::Expr>> {
let macro_file = self.body_source_map.as_ref()?.node_macro_file(expr.as_ref())?;
let expanded = db.parse_or_expand(macro_file)?;
let res = match ast::MacroCall::cast(expanded.clone()) {
Some(call) => self.expand_expr(db, InFile::new(macro_file, call))?,
_ => InFile::new(macro_file, ast::Expr::cast(expanded)?),
};
Some(res)
}
pub(crate) fn type_of_expr(&self, db: &dyn HirDatabase, expr: &ast::Expr) -> Option<Type> {
let expr_id = self.expr_id(db, expr)?;
let ty = self.infer.as_ref()?[expr_id].clone();
Type::new_with_resolver(db, &self.resolver, ty)
}
pub(crate) fn type_of_pat(&self, db: &dyn HirDatabase, pat: &ast::Pat) -> Option<Type> {
let pat_id = self.pat_id(pat)?;
let ty = self.infer.as_ref()?[pat_id].clone();
Type::new_with_resolver(db, &self.resolver, ty)
}
pub(crate) fn type_of_self(
&self,
db: &dyn HirDatabase,
param: &ast::SelfParam,
) -> Option<Type> {
let src = InFile { file_id: self.file_id, value: param };
let pat_id = self.body_source_map.as_ref()?.node_self_param(src)?;
let ty = self.infer.as_ref()?[pat_id].clone();
Type::new_with_resolver(db, &self.resolver, ty)
}
pub(crate) fn resolve_method_call(
&self,
db: &dyn HirDatabase,
call: &ast::MethodCallExpr,
) -> Option<FunctionId> {
let expr_id = self.expr_id(db, &call.clone().into())?;
self.infer.as_ref()?.method_resolution(expr_id)
}
pub(crate) fn resolve_field(
&self,
db: &dyn HirDatabase,
field: &ast::FieldExpr,
) -> Option<Field> {
let expr_id = self.expr_id(db, &field.clone().into())?;
self.infer.as_ref()?.field_resolution(expr_id).map(|it| it.into())
}
pub(crate) fn resolve_record_field(
&self,
db: &dyn HirDatabase,
field: &ast::RecordExprField,
) -> Option<(Field, Option<Local>)> {
let expr = field.expr()?;
let expr_id = self.expr_id(db, &expr)?;
let local = if field.name_ref().is_some() {
None
} else {
let local_name = field.field_name()?.as_name();
let path = ModPath::from_segments(PathKind::Plain, once(local_name));
match self.resolver.resolve_path_in_value_ns_fully(db.upcast(), &path) {
Some(ValueNs::LocalBinding(pat_id)) => {
Some(Local { pat_id, parent: self.resolver.body_owner()? })
}
_ => None,
}
};
let struct_field = self.infer.as_ref()?.record_field_resolution(expr_id)?;
Some((struct_field.into(), local))
}
pub(crate) fn resolve_record_field_pat(
&self,
_db: &dyn HirDatabase,
field: &ast::RecordPatField,
) -> Option<Field> {
let pat_id = self.pat_id(&field.pat()?)?;
let struct_field = self.infer.as_ref()?.record_field_pat_resolution(pat_id)?;
Some(struct_field.into())
}
pub(crate) fn resolve_macro_call(
&self,
db: &dyn HirDatabase,
macro_call: InFile<&ast::MacroCall>,
) -> Option<MacroDef> {
let hygiene = Hygiene::new(db.upcast(), macro_call.file_id);
let path = macro_call.value.path().and_then(|ast| Path::from_src(ast, &hygiene))?;
self.resolver.resolve_path_as_macro(db.upcast(), path.mod_path()).map(|it| it.into())
}
pub(crate) fn resolve_bind_pat_to_const(
&self,
db: &dyn HirDatabase,
pat: &ast::IdentPat,
) -> Option<ModuleDef> {
let pat_id = self.pat_id(&pat.clone().into())?;
let body = self.body.as_ref()?;
let path = match &body[pat_id] {
Pat::Path(path) => path,
_ => return None,
};
let res = resolve_hir_path(db, &self.resolver, &path)?;
match res {
PathResolution::Def(def) => Some(def),
_ => None,
}
}
pub(crate) fn resolve_path(
&self,
db: &dyn HirDatabase,
path: &ast::Path,
) -> Option<PathResolution> {
if let Some(path_expr) = path.syntax().parent().and_then(ast::PathExpr::cast) {
let expr_id = self.expr_id(db, &path_expr.into())?;
if let Some(assoc) = self.infer.as_ref()?.assoc_resolutions_for_expr(expr_id) {
return Some(PathResolution::AssocItem(assoc.into()));
}
if let Some(VariantId::EnumVariantId(variant)) =
self.infer.as_ref()?.variant_resolution_for_expr(expr_id)
{
return Some(PathResolution::Def(ModuleDef::EnumVariant(variant.into())));
}
}
if let Some(path_pat) = path.syntax().parent().and_then(ast::PathPat::cast) {
let pat_id = self.pat_id(&path_pat.into())?;
if let Some(assoc) = self.infer.as_ref()?.assoc_resolutions_for_pat(pat_id) {
return Some(PathResolution::AssocItem(assoc.into()));
}
if let Some(VariantId::EnumVariantId(variant)) =
self.infer.as_ref()?.variant_resolution_for_pat(pat_id)
{
return Some(PathResolution::Def(ModuleDef::EnumVariant(variant.into())));
}
}
if let Some(rec_lit) = path.syntax().parent().and_then(ast::RecordExpr::cast) {
let expr_id = self.expr_id(db, &rec_lit.into())?;
if let Some(VariantId::EnumVariantId(variant)) =
self.infer.as_ref()?.variant_resolution_for_expr(expr_id)
{
return Some(PathResolution::Def(ModuleDef::EnumVariant(variant.into())));
}
}
if let Some(rec_pat) = path.syntax().parent().and_then(ast::RecordPat::cast) {
let pat_id = self.pat_id(&rec_pat.into())?;
if let Some(VariantId::EnumVariantId(variant)) =
self.infer.as_ref()?.variant_resolution_for_pat(pat_id)
{
return Some(PathResolution::Def(ModuleDef::EnumVariant(variant.into())));
}
}
// This must be a normal source file rather than macro file.
let hir_path = Path::from_src(path.clone(), &Hygiene::new(db.upcast(), self.file_id))?;
// Case where path is a qualifier of another path, e.g. foo::bar::Baz where we
// trying to resolve foo::bar.
if let Some(outer_path) = path.syntax().parent().and_then(ast::Path::cast) {
if let Some(qualifier) = outer_path.qualifier() {
if path == &qualifier {
return resolve_hir_path_qualifier(db, &self.resolver, &hir_path);
}
}
}
resolve_hir_path(db, &self.resolver, &hir_path)
}
pub(crate) fn record_literal_missing_fields(
&self,
db: &dyn HirDatabase,
literal: &ast::RecordExpr,
) -> Option<Vec<(Field, Type)>> {
let krate = self.resolver.krate()?;
let body = self.body.as_ref()?;
let infer = self.infer.as_ref()?;
let expr_id = self.expr_id(db, &literal.clone().into())?;
let substs = match &infer.type_of_expr[expr_id] {
Ty::Apply(a_ty) => &a_ty.parameters,
_ => return None,
};
let (variant, missing_fields, _exhaustive) =
record_literal_missing_fields(db, infer, expr_id, &body[expr_id])?;
let res = self.missing_fields(db, krate, substs, variant, missing_fields);
Some(res)
}
pub(crate) fn record_pattern_missing_fields(
&self,
db: &dyn HirDatabase,
pattern: &ast::RecordPat,
) -> Option<Vec<(Field, Type)>> {
let krate = self.resolver.krate()?;
let body = self.body.as_ref()?;
let infer = self.infer.as_ref()?;
let pat_id = self.pat_id(&pattern.clone().into())?;
let substs = match &infer.type_of_pat[pat_id] {
Ty::Apply(a_ty) => &a_ty.parameters,
_ => return None,
};
let (variant, missing_fields, _exhaustive) =
record_pattern_missing_fields(db, infer, pat_id, &body[pat_id])?;
let res = self.missing_fields(db, krate, substs, variant, missing_fields);
Some(res)
}
fn missing_fields(
&self,
db: &dyn HirDatabase,
krate: CrateId,
substs: &Substs,
variant: VariantId,
missing_fields: Vec<LocalFieldId>,
) -> Vec<(Field, Type)> {
let field_types = db.field_types(variant);
missing_fields
.into_iter()
.map(|local_id| {
let field = FieldId { parent: variant, local_id };
let ty = field_types[local_id].clone().subst(substs);
(field.into(), Type::new_with_resolver_inner(db, krate, &self.resolver, ty))
})
.collect()
}
pub(crate) fn expand(
&self,
db: &dyn HirDatabase,
macro_call: InFile<&ast::MacroCall>,
) -> Option<HirFileId> {
let krate = self.resolver.krate()?;
let macro_call_id = macro_call.as_call_id(db.upcast(), krate, |path| {
self.resolver.resolve_path_as_macro(db.upcast(), &path)
})?;
Some(macro_call_id.as_file()).filter(|it| it.expansion_level(db.upcast()) < 64)
}
pub(crate) fn resolve_variant(
&self,
db: &dyn HirDatabase,
record_lit: ast::RecordExpr,
) -> Option<VariantId> {
let infer = self.infer.as_ref()?;
let expr_id = self.expr_id(db, &record_lit.into())?;
infer.variant_resolution_for_expr(expr_id)
}
}
fn scope_for(
scopes: &ExprScopes,
source_map: &BodySourceMap,
node: InFile<&SyntaxNode>,
) -> Option<ScopeId> {
node.value
.ancestors()
.filter_map(ast::Expr::cast)
.filter_map(|it| source_map.node_expr(InFile::new(node.file_id, &it)))
.find_map(|it| scopes.scope_for(it))
}
fn scope_for_offset(
db: &dyn HirDatabase,
scopes: &ExprScopes,
source_map: &BodySourceMap,
offset: InFile<TextSize>,
) -> Option<ScopeId> {
scopes
.scope_by_expr()
.iter()
.filter_map(|(id, scope)| {
let source = source_map.expr_syntax(*id).ok()?;
// FIXME: correctly handle macro expansion
if source.file_id != offset.file_id {
return None;
}
let root = source.file_syntax(db.upcast());
let node = source.value.to_node(&root);
Some((node.syntax().text_range(), scope))
})
// find containing scope
.min_by_key(|(expr_range, _scope)| {
(
!(expr_range.start() <= offset.value && offset.value <= expr_range.end()),
expr_range.len(),
)
})
.map(|(expr_range, scope)| {
adjust(db, scopes, source_map, expr_range, offset).unwrap_or(*scope)
})
}
// XXX: during completion, cursor might be outside of any particular
// expression. Try to figure out the correct scope...
fn adjust(
db: &dyn HirDatabase,
scopes: &ExprScopes,
source_map: &BodySourceMap,
expr_range: TextRange,
offset: InFile<TextSize>,
) -> Option<ScopeId> {
let child_scopes = scopes
.scope_by_expr()
.iter()
.filter_map(|(id, scope)| {
let source = source_map.expr_syntax(*id).ok()?;
// FIXME: correctly handle macro expansion
if source.file_id != offset.file_id {
return None;
}
let root = source.file_syntax(db.upcast());
let node = source.value.to_node(&root);
Some((node.syntax().text_range(), scope))
})
.filter(|&(range, _)| {
range.start() <= offset.value && expr_range.contains_range(range) && range != expr_range
});
child_scopes
.max_by(|&(r1, _), &(r2, _)| {
if r1.contains_range(r2) {
std::cmp::Ordering::Greater
} else if r2.contains_range(r1) {
std::cmp::Ordering::Less
} else {
r1.start().cmp(&r2.start())
}
})
.map(|(_ptr, scope)| *scope)
}
pub(crate) fn resolve_hir_path(
db: &dyn HirDatabase,
resolver: &Resolver,
path: &Path,
) -> Option<PathResolution> {
let types =
resolver.resolve_path_in_type_ns_fully(db.upcast(), path.mod_path()).map(|ty| match ty {
TypeNs::SelfType(it) => PathResolution::SelfType(it.into()),
TypeNs::GenericParam(id) => PathResolution::TypeParam(TypeParam { id }),
TypeNs::AdtSelfType(it) | TypeNs::AdtId(it) => {
PathResolution::Def(Adt::from(it).into())
}
TypeNs::EnumVariantId(it) => PathResolution::Def(EnumVariant::from(it).into()),
TypeNs::TypeAliasId(it) => PathResolution::Def(TypeAlias::from(it).into()),
TypeNs::BuiltinType(it) => PathResolution::Def(it.into()),
TypeNs::TraitId(it) => PathResolution::Def(Trait::from(it).into()),
});
let body_owner = resolver.body_owner();
let values =
resolver.resolve_path_in_value_ns_fully(db.upcast(), path.mod_path()).and_then(|val| {
let res = match val {
ValueNs::LocalBinding(pat_id) => {
let var = Local { parent: body_owner?.into(), pat_id };
PathResolution::Local(var)
}
ValueNs::FunctionId(it) => PathResolution::Def(Function::from(it).into()),
ValueNs::ConstId(it) => PathResolution::Def(Const::from(it).into()),
ValueNs::StaticId(it) => PathResolution::Def(Static::from(it).into()),
ValueNs::StructId(it) => PathResolution::Def(Struct::from(it).into()),
ValueNs::EnumVariantId(it) => PathResolution::Def(EnumVariant::from(it).into()),
ValueNs::ImplSelf(impl_id) => PathResolution::SelfType(impl_id.into()),
};
Some(res)
});
let items = resolver
.resolve_module_path_in_items(db.upcast(), path.mod_path())
.take_types()
.map(|it| PathResolution::Def(it.into()));
types.or(values).or(items).or_else(|| {
resolver
.resolve_path_as_macro(db.upcast(), path.mod_path())
.map(|def| PathResolution::Macro(def.into()))
})
}
/// Resolves a path where we know it is a qualifier of another path.
///
/// For example, if we have:
/// ```
/// mod my {
/// pub mod foo {
/// struct Bar;
/// }
///
/// pub fn foo() {}
/// }
/// ```
/// then we know that `foo` in `my::foo::Bar` refers to the module, not the function.
fn resolve_hir_path_qualifier(
db: &dyn HirDatabase,
resolver: &Resolver,
path: &Path,
) -> Option<PathResolution> {
let items = resolver
.resolve_module_path_in_items(db.upcast(), path.mod_path())
.take_types()
.map(|it| PathResolution::Def(it.into()));
if items.is_some() {
return items;
}
resolver.resolve_path_in_type_ns_fully(db.upcast(), path.mod_path()).map(|ty| match ty {
TypeNs::SelfType(it) => PathResolution::SelfType(it.into()),
TypeNs::GenericParam(id) => PathResolution::TypeParam(TypeParam { id }),
TypeNs::AdtSelfType(it) | TypeNs::AdtId(it) => PathResolution::Def(Adt::from(it).into()),
TypeNs::EnumVariantId(it) => PathResolution::Def(EnumVariant::from(it).into()),
TypeNs::TypeAliasId(it) => PathResolution::Def(TypeAlias::from(it).into()),
TypeNs::BuiltinType(it) => PathResolution::Def(it.into()),
TypeNs::TraitId(it) => PathResolution::Def(Trait::from(it).into()),
})
}

35
crates/hir_def/Cargo.toml Normal file
View file

@ -0,0 +1,35 @@
[package]
name = "hir_def"
version = "0.0.0"
license = "MIT OR Apache-2.0"
authors = ["rust-analyzer developers"]
edition = "2018"
[lib]
doctest = false
[dependencies]
log = "0.4.8"
once_cell = "1.3.1"
rustc-hash = "1.1.0"
either = "1.5.3"
anymap = "0.12.1"
drop_bomb = "0.1.4"
fst = { version = "0.4", default-features = false }
itertools = "0.9.0"
indexmap = "1.4.0"
smallvec = "1.4.0"
stdx = { path = "../stdx" }
arena = { path = "../arena" }
base_db = { path = "../base_db" }
syntax = { path = "../syntax" }
profile = { path = "../profile" }
hir_expand = { path = "../hir_expand" }
test_utils = { path = "../test_utils" }
mbe = { path = "../mbe" }
cfg = { path = "../cfg" }
tt = { path = "../tt" }
[dev-dependencies]
expect-test = "0.1"

329
crates/hir_def/src/adt.rs Normal file
View file

@ -0,0 +1,329 @@
//! Defines hir-level representation of structs, enums and unions
use std::sync::Arc;
use arena::{map::ArenaMap, Arena};
use either::Either;
use hir_expand::{
name::{AsName, Name},
InFile,
};
use syntax::ast::{self, NameOwner, VisibilityOwner};
use tt::{Delimiter, DelimiterKind, Leaf, Subtree, TokenTree};
use crate::{
body::{CfgExpander, LowerCtx},
db::DefDatabase,
item_tree::{AttrOwner, Field, Fields, ItemTree, ModItem},
src::HasChildSource,
src::HasSource,
trace::Trace,
type_ref::TypeRef,
visibility::RawVisibility,
EnumId, HasModule, LocalEnumVariantId, LocalFieldId, Lookup, ModuleId, StructId, UnionId,
VariantId,
};
use cfg::CfgOptions;
/// Note that we use `StructData` for unions as well!
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StructData {
pub name: Name,
pub variant_data: Arc<VariantData>,
pub repr: Option<ReprKind>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EnumData {
pub name: Name,
pub variants: Arena<EnumVariantData>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EnumVariantData {
pub name: Name,
pub variant_data: Arc<VariantData>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum VariantData {
Record(Arena<FieldData>),
Tuple(Arena<FieldData>),
Unit,
}
/// A single field of an enum variant or struct
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FieldData {
pub name: Name,
pub type_ref: TypeRef,
pub visibility: RawVisibility,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ReprKind {
Packed,
Other,
}
fn repr_from_value(item_tree: &ItemTree, of: AttrOwner) -> Option<ReprKind> {
item_tree.attrs(of).by_key("repr").tt_values().find_map(parse_repr_tt)
}
fn parse_repr_tt(tt: &Subtree) -> Option<ReprKind> {
match tt.delimiter {
Some(Delimiter { kind: DelimiterKind::Parenthesis, .. }) => {}
_ => return None,
}
let mut it = tt.token_trees.iter();
match it.next()? {
TokenTree::Leaf(Leaf::Ident(ident)) if ident.text == "packed" => Some(ReprKind::Packed),
_ => Some(ReprKind::Other),
}
}
impl StructData {
pub(crate) fn struct_data_query(db: &dyn DefDatabase, id: StructId) -> Arc<StructData> {
let loc = id.lookup(db);
let item_tree = db.item_tree(loc.id.file_id);
let repr = repr_from_value(&item_tree, ModItem::from(loc.id.value).into());
let cfg_options = db.crate_graph()[loc.container.module(db).krate].cfg_options.clone();
let strukt = &item_tree[loc.id.value];
let variant_data = lower_fields(&item_tree, &cfg_options, &strukt.fields);
Arc::new(StructData {
name: strukt.name.clone(),
variant_data: Arc::new(variant_data),
repr,
})
}
pub(crate) fn union_data_query(db: &dyn DefDatabase, id: UnionId) -> Arc<StructData> {
let loc = id.lookup(db);
let item_tree = db.item_tree(loc.id.file_id);
let repr = repr_from_value(&item_tree, ModItem::from(loc.id.value).into());
let cfg_options = db.crate_graph()[loc.container.module(db).krate].cfg_options.clone();
let union = &item_tree[loc.id.value];
let variant_data = lower_fields(&item_tree, &cfg_options, &union.fields);
Arc::new(StructData {
name: union.name.clone(),
variant_data: Arc::new(variant_data),
repr,
})
}
}
impl EnumData {
pub(crate) fn enum_data_query(db: &dyn DefDatabase, e: EnumId) -> Arc<EnumData> {
let loc = e.lookup(db);
let item_tree = db.item_tree(loc.id.file_id);
let cfg_options = db.crate_graph()[loc.container.module(db).krate].cfg_options.clone();
let enum_ = &item_tree[loc.id.value];
let mut variants = Arena::new();
for var_id in enum_.variants.clone() {
if item_tree.attrs(var_id.into()).is_cfg_enabled(&cfg_options) {
let var = &item_tree[var_id];
let var_data = lower_fields(&item_tree, &cfg_options, &var.fields);
variants.alloc(EnumVariantData {
name: var.name.clone(),
variant_data: Arc::new(var_data),
});
}
}
Arc::new(EnumData { name: enum_.name.clone(), variants })
}
pub fn variant(&self, name: &Name) -> Option<LocalEnumVariantId> {
let (id, _) = self.variants.iter().find(|(_id, data)| &data.name == name)?;
Some(id)
}
}
impl HasChildSource for EnumId {
type ChildId = LocalEnumVariantId;
type Value = ast::Variant;
fn child_source(&self, db: &dyn DefDatabase) -> InFile<ArenaMap<Self::ChildId, Self::Value>> {
let src = self.lookup(db).source(db);
let mut trace = Trace::new_for_map();
lower_enum(db, &mut trace, &src, self.lookup(db).container.module(db));
src.with_value(trace.into_map())
}
}
fn lower_enum(
db: &dyn DefDatabase,
trace: &mut Trace<EnumVariantData, ast::Variant>,
ast: &InFile<ast::Enum>,
module_id: ModuleId,
) {
let expander = CfgExpander::new(db, ast.file_id, module_id.krate);
let variants = ast
.value
.variant_list()
.into_iter()
.flat_map(|it| it.variants())
.filter(|var| expander.is_cfg_enabled(var));
for var in variants {
trace.alloc(
|| var.clone(),
|| EnumVariantData {
name: var.name().map_or_else(Name::missing, |it| it.as_name()),
variant_data: Arc::new(VariantData::new(db, ast.with_value(var.kind()), module_id)),
},
);
}
}
impl VariantData {
fn new(db: &dyn DefDatabase, flavor: InFile<ast::StructKind>, module_id: ModuleId) -> Self {
let mut expander = CfgExpander::new(db, flavor.file_id, module_id.krate);
let mut trace = Trace::new_for_arena();
match lower_struct(db, &mut expander, &mut trace, &flavor) {
StructKind::Tuple => VariantData::Tuple(trace.into_arena()),
StructKind::Record => VariantData::Record(trace.into_arena()),
StructKind::Unit => VariantData::Unit,
}
}
pub fn fields(&self) -> &Arena<FieldData> {
const EMPTY: &Arena<FieldData> = &Arena::new();
match &self {
VariantData::Record(fields) | VariantData::Tuple(fields) => fields,
_ => EMPTY,
}
}
pub fn field(&self, name: &Name) -> Option<LocalFieldId> {
self.fields().iter().find_map(|(id, data)| if &data.name == name { Some(id) } else { None })
}
pub fn kind(&self) -> StructKind {
match self {
VariantData::Record(_) => StructKind::Record,
VariantData::Tuple(_) => StructKind::Tuple,
VariantData::Unit => StructKind::Unit,
}
}
}
impl HasChildSource for VariantId {
type ChildId = LocalFieldId;
type Value = Either<ast::TupleField, ast::RecordField>;
fn child_source(&self, db: &dyn DefDatabase) -> InFile<ArenaMap<Self::ChildId, Self::Value>> {
let (src, module_id) = match self {
VariantId::EnumVariantId(it) => {
// I don't really like the fact that we call into parent source
// here, this might add to more queries then necessary.
let src = it.parent.child_source(db);
(src.map(|map| map[it.local_id].kind()), it.parent.lookup(db).container.module(db))
}
VariantId::StructId(it) => {
(it.lookup(db).source(db).map(|it| it.kind()), it.lookup(db).container.module(db))
}
VariantId::UnionId(it) => (
it.lookup(db).source(db).map(|it| {
it.record_field_list()
.map(ast::StructKind::Record)
.unwrap_or(ast::StructKind::Unit)
}),
it.lookup(db).container.module(db),
),
};
let mut expander = CfgExpander::new(db, src.file_id, module_id.krate);
let mut trace = Trace::new_for_map();
lower_struct(db, &mut expander, &mut trace, &src);
src.with_value(trace.into_map())
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum StructKind {
Tuple,
Record,
Unit,
}
fn lower_struct(
db: &dyn DefDatabase,
expander: &mut CfgExpander,
trace: &mut Trace<FieldData, Either<ast::TupleField, ast::RecordField>>,
ast: &InFile<ast::StructKind>,
) -> StructKind {
let ctx = LowerCtx::new(db, ast.file_id);
match &ast.value {
ast::StructKind::Tuple(fl) => {
for (i, fd) in fl.fields().enumerate() {
if !expander.is_cfg_enabled(&fd) {
continue;
}
trace.alloc(
|| Either::Left(fd.clone()),
|| FieldData {
name: Name::new_tuple_field(i),
type_ref: TypeRef::from_ast_opt(&ctx, fd.ty()),
visibility: RawVisibility::from_ast(db, ast.with_value(fd.visibility())),
},
);
}
StructKind::Tuple
}
ast::StructKind::Record(fl) => {
for fd in fl.fields() {
if !expander.is_cfg_enabled(&fd) {
continue;
}
trace.alloc(
|| Either::Right(fd.clone()),
|| FieldData {
name: fd.name().map(|n| n.as_name()).unwrap_or_else(Name::missing),
type_ref: TypeRef::from_ast_opt(&ctx, fd.ty()),
visibility: RawVisibility::from_ast(db, ast.with_value(fd.visibility())),
},
);
}
StructKind::Record
}
ast::StructKind::Unit => StructKind::Unit,
}
}
fn lower_fields(item_tree: &ItemTree, cfg_options: &CfgOptions, fields: &Fields) -> VariantData {
match fields {
Fields::Record(flds) => {
let mut arena = Arena::new();
for field_id in flds.clone() {
if item_tree.attrs(field_id.into()).is_cfg_enabled(cfg_options) {
arena.alloc(lower_field(item_tree, &item_tree[field_id]));
}
}
VariantData::Record(arena)
}
Fields::Tuple(flds) => {
let mut arena = Arena::new();
for field_id in flds.clone() {
if item_tree.attrs(field_id.into()).is_cfg_enabled(cfg_options) {
arena.alloc(lower_field(item_tree, &item_tree[field_id]));
}
}
VariantData::Tuple(arena)
}
Fields::Unit => VariantData::Unit,
}
}
fn lower_field(item_tree: &ItemTree, field: &Field) -> FieldData {
FieldData {
name: field.name.clone(),
type_ref: field.type_ref.clone(),
visibility: item_tree[field.visibility].clone(),
}
}

212
crates/hir_def/src/attr.rs Normal file
View file

@ -0,0 +1,212 @@
//! A higher level attributes based on TokenTree, with also some shortcuts.
use std::{ops, sync::Arc};
use cfg::{CfgExpr, CfgOptions};
use either::Either;
use hir_expand::{hygiene::Hygiene, AstId, InFile};
use mbe::ast_to_token_tree;
use syntax::{
ast::{self, AstNode, AttrsOwner},
SmolStr,
};
use tt::Subtree;
use crate::{
db::DefDatabase,
item_tree::{ItemTreeId, ItemTreeNode},
nameres::ModuleSource,
path::ModPath,
src::HasChildSource,
AdtId, AttrDefId, Lookup,
};
#[derive(Default, Debug, Clone, PartialEq, Eq)]
pub struct Attrs {
entries: Option<Arc<[Attr]>>,
}
impl ops::Deref for Attrs {
type Target = [Attr];
fn deref(&self) -> &[Attr] {
match &self.entries {
Some(it) => &*it,
None => &[],
}
}
}
impl Attrs {
pub const EMPTY: Attrs = Attrs { entries: None };
pub(crate) fn attrs_query(db: &dyn DefDatabase, def: AttrDefId) -> Attrs {
match def {
AttrDefId::ModuleId(module) => {
let def_map = db.crate_def_map(module.krate);
let mod_data = &def_map[module.local_id];
match mod_data.declaration_source(db) {
Some(it) => {
Attrs::from_attrs_owner(db, it.as_ref().map(|it| it as &dyn AttrsOwner))
}
None => Attrs::from_attrs_owner(
db,
mod_data.definition_source(db).as_ref().map(|src| match src {
ModuleSource::SourceFile(file) => file as &dyn AttrsOwner,
ModuleSource::Module(module) => module as &dyn AttrsOwner,
}),
),
}
}
AttrDefId::FieldId(it) => {
let src = it.parent.child_source(db);
match &src.value[it.local_id] {
Either::Left(_tuple) => Attrs::default(),
Either::Right(record) => Attrs::from_attrs_owner(db, src.with_value(record)),
}
}
AttrDefId::EnumVariantId(var_id) => {
let src = var_id.parent.child_source(db);
let src = src.as_ref().map(|it| &it[var_id.local_id]);
Attrs::from_attrs_owner(db, src.map(|it| it as &dyn AttrsOwner))
}
AttrDefId::AdtId(it) => match it {
AdtId::StructId(it) => attrs_from_item_tree(it.lookup(db).id, db),
AdtId::EnumId(it) => attrs_from_item_tree(it.lookup(db).id, db),
AdtId::UnionId(it) => attrs_from_item_tree(it.lookup(db).id, db),
},
AttrDefId::TraitId(it) => attrs_from_item_tree(it.lookup(db).id, db),
AttrDefId::MacroDefId(it) => {
it.ast_id.map_or_else(Default::default, |ast_id| attrs_from_ast(ast_id, db))
}
AttrDefId::ImplId(it) => attrs_from_item_tree(it.lookup(db).id, db),
AttrDefId::ConstId(it) => attrs_from_item_tree(it.lookup(db).id, db),
AttrDefId::StaticId(it) => attrs_from_item_tree(it.lookup(db).id, db),
AttrDefId::FunctionId(it) => attrs_from_item_tree(it.lookup(db).id, db),
AttrDefId::TypeAliasId(it) => attrs_from_item_tree(it.lookup(db).id, db),
}
}
pub fn from_attrs_owner(db: &dyn DefDatabase, owner: InFile<&dyn AttrsOwner>) -> Attrs {
let hygiene = Hygiene::new(db.upcast(), owner.file_id);
Attrs::new(owner.value, &hygiene)
}
pub(crate) fn new(owner: &dyn AttrsOwner, hygiene: &Hygiene) -> Attrs {
let docs = ast::CommentIter::from_syntax_node(owner.syntax()).doc_comment_text().map(
|docs_text| Attr {
input: Some(AttrInput::Literal(SmolStr::new(docs_text))),
path: ModPath::from(hir_expand::name!(doc)),
},
);
let mut attrs = owner.attrs().peekable();
let entries = if attrs.peek().is_none() {
// Avoid heap allocation
None
} else {
Some(attrs.flat_map(|ast| Attr::from_src(ast, hygiene)).chain(docs).collect())
};
Attrs { entries }
}
pub fn merge(&self, other: Attrs) -> Attrs {
match (&self.entries, &other.entries) {
(None, None) => Attrs { entries: None },
(Some(entries), None) | (None, Some(entries)) => {
Attrs { entries: Some(entries.clone()) }
}
(Some(a), Some(b)) => {
Attrs { entries: Some(a.iter().chain(b.iter()).cloned().collect()) }
}
}
}
pub fn by_key(&self, key: &'static str) -> AttrQuery<'_> {
AttrQuery { attrs: self, key }
}
pub fn cfg(&self) -> impl Iterator<Item = CfgExpr> + '_ {
// FIXME: handle cfg_attr :-)
self.by_key("cfg").tt_values().map(CfgExpr::parse)
}
pub(crate) fn is_cfg_enabled(&self, cfg_options: &CfgOptions) -> bool {
self.cfg().all(|cfg| cfg_options.check(&cfg) != Some(false))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Attr {
pub(crate) path: ModPath,
pub(crate) input: Option<AttrInput>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AttrInput {
/// `#[attr = "string"]`
Literal(SmolStr),
/// `#[attr(subtree)]`
TokenTree(Subtree),
}
impl Attr {
fn from_src(ast: ast::Attr, hygiene: &Hygiene) -> Option<Attr> {
let path = ModPath::from_src(ast.path()?, hygiene)?;
let input = if let Some(lit) = ast.literal() {
// FIXME: escape? raw string?
let value = lit.syntax().first_token()?.text().trim_matches('"').into();
Some(AttrInput::Literal(value))
} else if let Some(tt) = ast.token_tree() {
Some(AttrInput::TokenTree(ast_to_token_tree(&tt)?.0))
} else {
None
};
Some(Attr { path, input })
}
}
#[derive(Debug, Clone, Copy)]
pub struct AttrQuery<'a> {
attrs: &'a Attrs,
key: &'static str,
}
impl<'a> AttrQuery<'a> {
pub fn tt_values(self) -> impl Iterator<Item = &'a Subtree> {
self.attrs().filter_map(|attr| match attr.input.as_ref()? {
AttrInput::TokenTree(it) => Some(it),
_ => None,
})
}
pub fn string_value(self) -> Option<&'a SmolStr> {
self.attrs().find_map(|attr| match attr.input.as_ref()? {
AttrInput::Literal(it) => Some(it),
_ => None,
})
}
pub fn exists(self) -> bool {
self.attrs().next().is_some()
}
fn attrs(self) -> impl Iterator<Item = &'a Attr> {
let key = self.key;
self.attrs
.iter()
.filter(move |attr| attr.path.as_ident().map_or(false, |s| s.to_string() == key))
}
}
fn attrs_from_ast<N>(src: AstId<N>, db: &dyn DefDatabase) -> Attrs
where
N: ast::AttrsOwner,
{
let src = InFile::new(src.file_id, src.to_node(db.upcast()));
Attrs::from_attrs_owner(db, src.as_ref().map(|it| it as &dyn AttrsOwner))
}
fn attrs_from_item_tree<N: ItemTreeNode>(id: ItemTreeId<N>, db: &dyn DefDatabase) -> Attrs {
let tree = db.item_tree(id.file_id);
let mod_item = N::id_to_mod_item(id.value);
tree.attrs(mod_item.into()).clone()
}

360
crates/hir_def/src/body.rs Normal file
View file

@ -0,0 +1,360 @@
//! Defines `Body`: a lowered representation of bodies of functions, statics and
//! consts.
mod lower;
pub mod scope;
use std::{mem, ops::Index, sync::Arc};
use arena::{map::ArenaMap, Arena};
use base_db::CrateId;
use cfg::CfgOptions;
use drop_bomb::DropBomb;
use either::Either;
use hir_expand::{ast_id_map::AstIdMap, hygiene::Hygiene, AstId, HirFileId, InFile, MacroDefId};
use rustc_hash::FxHashMap;
use syntax::{ast, AstNode, AstPtr};
use test_utils::mark;
pub(crate) use lower::LowerCtx;
use crate::{
attr::Attrs,
db::DefDatabase,
expr::{Expr, ExprId, Pat, PatId},
item_scope::BuiltinShadowMode,
item_scope::ItemScope,
nameres::CrateDefMap,
path::{ModPath, Path},
src::HasSource,
AsMacroCall, DefWithBodyId, HasModule, Lookup, ModuleId,
};
/// A subset of Expander that only deals with cfg attributes. We only need it to
/// avoid cyclic queries in crate def map during enum processing.
pub(crate) struct CfgExpander {
cfg_options: CfgOptions,
hygiene: Hygiene,
}
pub(crate) struct Expander {
cfg_expander: CfgExpander,
crate_def_map: Arc<CrateDefMap>,
current_file_id: HirFileId,
ast_id_map: Arc<AstIdMap>,
module: ModuleId,
recursion_limit: usize,
}
#[cfg(test)]
const EXPANSION_RECURSION_LIMIT: usize = 32;
#[cfg(not(test))]
const EXPANSION_RECURSION_LIMIT: usize = 128;
impl CfgExpander {
pub(crate) fn new(
db: &dyn DefDatabase,
current_file_id: HirFileId,
krate: CrateId,
) -> CfgExpander {
let hygiene = Hygiene::new(db.upcast(), current_file_id);
let cfg_options = db.crate_graph()[krate].cfg_options.clone();
CfgExpander { cfg_options, hygiene }
}
pub(crate) fn parse_attrs(&self, owner: &dyn ast::AttrsOwner) -> Attrs {
Attrs::new(owner, &self.hygiene)
}
pub(crate) fn is_cfg_enabled(&self, owner: &dyn ast::AttrsOwner) -> bool {
let attrs = self.parse_attrs(owner);
attrs.is_cfg_enabled(&self.cfg_options)
}
}
impl Expander {
pub(crate) fn new(
db: &dyn DefDatabase,
current_file_id: HirFileId,
module: ModuleId,
) -> Expander {
let cfg_expander = CfgExpander::new(db, current_file_id, module.krate);
let crate_def_map = db.crate_def_map(module.krate);
let ast_id_map = db.ast_id_map(current_file_id);
Expander {
cfg_expander,
crate_def_map,
current_file_id,
ast_id_map,
module,
recursion_limit: 0,
}
}
pub(crate) fn enter_expand<T: ast::AstNode>(
&mut self,
db: &dyn DefDatabase,
local_scope: Option<&ItemScope>,
macro_call: ast::MacroCall,
) -> Option<(Mark, T)> {
self.recursion_limit += 1;
if self.recursion_limit > EXPANSION_RECURSION_LIMIT {
mark::hit!(your_stack_belongs_to_me);
return None;
}
let macro_call = InFile::new(self.current_file_id, &macro_call);
if let Some(call_id) = macro_call.as_call_id(db, self.crate_def_map.krate, |path| {
if let Some(local_scope) = local_scope {
if let Some(def) = path.as_ident().and_then(|n| local_scope.get_legacy_macro(n)) {
return Some(def);
}
}
self.resolve_path_as_macro(db, &path)
}) {
let file_id = call_id.as_file();
if let Some(node) = db.parse_or_expand(file_id) {
if let Some(expr) = T::cast(node) {
log::debug!("macro expansion {:#?}", expr.syntax());
let mark = Mark {
file_id: self.current_file_id,
ast_id_map: mem::take(&mut self.ast_id_map),
bomb: DropBomb::new("expansion mark dropped"),
};
self.cfg_expander.hygiene = Hygiene::new(db.upcast(), file_id);
self.current_file_id = file_id;
self.ast_id_map = db.ast_id_map(file_id);
return Some((mark, expr));
}
}
}
// FIXME: Instead of just dropping the error from expansion
// report it
None
}
pub(crate) fn exit(&mut self, db: &dyn DefDatabase, mut mark: Mark) {
self.cfg_expander.hygiene = Hygiene::new(db.upcast(), mark.file_id);
self.current_file_id = mark.file_id;
self.ast_id_map = mem::take(&mut mark.ast_id_map);
self.recursion_limit -= 1;
mark.bomb.defuse();
}
pub(crate) fn to_source<T>(&self, value: T) -> InFile<T> {
InFile { file_id: self.current_file_id, value }
}
pub(crate) fn is_cfg_enabled(&self, owner: &dyn ast::AttrsOwner) -> bool {
self.cfg_expander.is_cfg_enabled(owner)
}
fn parse_path(&mut self, path: ast::Path) -> Option<Path> {
Path::from_src(path, &self.cfg_expander.hygiene)
}
fn resolve_path_as_macro(&self, db: &dyn DefDatabase, path: &ModPath) -> Option<MacroDefId> {
self.crate_def_map
.resolve_path(db, self.module.local_id, path, BuiltinShadowMode::Other)
.0
.take_macros()
}
fn ast_id<N: AstNode>(&self, item: &N) -> AstId<N> {
let file_local_id = self.ast_id_map.ast_id(item);
AstId::new(self.current_file_id, file_local_id)
}
}
pub(crate) struct Mark {
file_id: HirFileId,
ast_id_map: Arc<AstIdMap>,
bomb: DropBomb,
}
/// The body of an item (function, const etc.).
#[derive(Debug, Eq, PartialEq)]
pub struct Body {
pub exprs: Arena<Expr>,
pub pats: Arena<Pat>,
/// The patterns for the function's parameters. While the parameter types are
/// part of the function signature, the patterns are not (they don't change
/// the external type of the function).
///
/// If this `Body` is for the body of a constant, this will just be
/// empty.
pub params: Vec<PatId>,
/// The `ExprId` of the actual body expression.
pub body_expr: ExprId,
pub item_scope: ItemScope,
}
pub type ExprPtr = AstPtr<ast::Expr>;
pub type ExprSource = InFile<ExprPtr>;
pub type PatPtr = Either<AstPtr<ast::Pat>, AstPtr<ast::SelfParam>>;
pub type PatSource = InFile<PatPtr>;
/// An item body together with the mapping from syntax nodes to HIR expression
/// IDs. This is needed to go from e.g. a position in a file to the HIR
/// expression containing it; but for type inference etc., we want to operate on
/// a structure that is agnostic to the actual positions of expressions in the
/// file, so that we don't recompute types whenever some whitespace is typed.
///
/// One complication here is that, due to macro expansion, a single `Body` might
/// be spread across several files. So, for each ExprId and PatId, we record
/// both the HirFileId and the position inside the file. However, we only store
/// AST -> ExprId mapping for non-macro files, as it is not clear how to handle
/// this properly for macros.
#[derive(Default, Debug, Eq, PartialEq)]
pub struct BodySourceMap {
expr_map: FxHashMap<ExprSource, ExprId>,
expr_map_back: ArenaMap<ExprId, Result<ExprSource, SyntheticSyntax>>,
pat_map: FxHashMap<PatSource, PatId>,
pat_map_back: ArenaMap<PatId, Result<PatSource, SyntheticSyntax>>,
field_map: FxHashMap<(ExprId, usize), InFile<AstPtr<ast::RecordExprField>>>,
expansions: FxHashMap<InFile<AstPtr<ast::MacroCall>>, HirFileId>,
}
#[derive(Default, Debug, Eq, PartialEq, Clone, Copy)]
pub struct SyntheticSyntax;
impl Body {
pub(crate) fn body_with_source_map_query(
db: &dyn DefDatabase,
def: DefWithBodyId,
) -> (Arc<Body>, Arc<BodySourceMap>) {
let _p = profile::span("body_with_source_map_query");
let mut params = None;
let (file_id, module, body) = match def {
DefWithBodyId::FunctionId(f) => {
let f = f.lookup(db);
let src = f.source(db);
params = src.value.param_list();
(src.file_id, f.module(db), src.value.body().map(ast::Expr::from))
}
DefWithBodyId::ConstId(c) => {
let c = c.lookup(db);
let src = c.source(db);
(src.file_id, c.module(db), src.value.body())
}
DefWithBodyId::StaticId(s) => {
let s = s.lookup(db);
let src = s.source(db);
(src.file_id, s.module(db), src.value.body())
}
};
let expander = Expander::new(db, file_id, module);
let (body, source_map) = Body::new(db, def, expander, params, body);
(Arc::new(body), Arc::new(source_map))
}
pub(crate) fn body_query(db: &dyn DefDatabase, def: DefWithBodyId) -> Arc<Body> {
db.body_with_source_map(def).0
}
fn new(
db: &dyn DefDatabase,
def: DefWithBodyId,
expander: Expander,
params: Option<ast::ParamList>,
body: Option<ast::Expr>,
) -> (Body, BodySourceMap) {
lower::lower(db, def, expander, params, body)
}
}
impl Index<ExprId> for Body {
type Output = Expr;
fn index(&self, expr: ExprId) -> &Expr {
&self.exprs[expr]
}
}
impl Index<PatId> for Body {
type Output = Pat;
fn index(&self, pat: PatId) -> &Pat {
&self.pats[pat]
}
}
impl BodySourceMap {
pub fn expr_syntax(&self, expr: ExprId) -> Result<ExprSource, SyntheticSyntax> {
self.expr_map_back[expr].clone()
}
pub fn node_expr(&self, node: InFile<&ast::Expr>) -> Option<ExprId> {
let src = node.map(|it| AstPtr::new(it));
self.expr_map.get(&src).cloned()
}
pub fn node_macro_file(&self, node: InFile<&ast::MacroCall>) -> Option<HirFileId> {
let src = node.map(|it| AstPtr::new(it));
self.expansions.get(&src).cloned()
}
pub fn pat_syntax(&self, pat: PatId) -> Result<PatSource, SyntheticSyntax> {
self.pat_map_back[pat].clone()
}
pub fn node_pat(&self, node: InFile<&ast::Pat>) -> Option<PatId> {
let src = node.map(|it| Either::Left(AstPtr::new(it)));
self.pat_map.get(&src).cloned()
}
pub fn node_self_param(&self, node: InFile<&ast::SelfParam>) -> Option<PatId> {
let src = node.map(|it| Either::Right(AstPtr::new(it)));
self.pat_map.get(&src).cloned()
}
pub fn field_syntax(&self, expr: ExprId, field: usize) -> InFile<AstPtr<ast::RecordExprField>> {
self.field_map[&(expr, field)].clone()
}
}
#[cfg(test)]
mod tests {
use base_db::{fixture::WithFixture, SourceDatabase};
use test_utils::mark;
use crate::ModuleDefId;
use super::*;
fn lower(ra_fixture: &str) -> Arc<Body> {
let (db, file_id) = crate::test_db::TestDB::with_single_file(ra_fixture);
let krate = db.crate_graph().iter().next().unwrap();
let def_map = db.crate_def_map(krate);
let module = def_map.modules_for_file(file_id).next().unwrap();
let module = &def_map[module];
let fn_def = match module.scope.declarations().next().unwrap() {
ModuleDefId::FunctionId(it) => it,
_ => panic!(),
};
db.body(fn_def.into())
}
#[test]
fn your_stack_belongs_to_me() {
mark::check!(your_stack_belongs_to_me);
lower(
"
macro_rules! n_nuple {
($e:tt) => ();
($($rest:tt)*) => {{
(n_nuple!($($rest)*)None,)
}};
}
fn main() { n_nuple!(1,2,3); }
",
);
}
}

View file

@ -0,0 +1,931 @@
//! Transforms `ast::Expr` into an equivalent `hir_def::expr::Expr`
//! representation.
use std::{any::type_name, sync::Arc};
use arena::Arena;
use either::Either;
use hir_expand::{
hygiene::Hygiene,
name::{name, AsName, Name},
HirFileId, MacroDefId, MacroDefKind,
};
use rustc_hash::FxHashMap;
use syntax::{
ast::{
self, ArgListOwner, ArrayExprKind, AstChildren, LiteralKind, LoopBodyOwner, NameOwner,
SlicePatComponents,
},
AstNode, AstPtr,
};
use test_utils::mark;
use crate::{
adt::StructKind,
body::{Body, BodySourceMap, Expander, PatPtr, SyntheticSyntax},
builtin_type::{BuiltinFloat, BuiltinInt},
db::DefDatabase,
expr::{
dummy_expr_id, ArithOp, Array, BinaryOp, BindingAnnotation, CmpOp, Expr, ExprId, Literal,
LogicOp, MatchArm, Ordering, Pat, PatId, RecordFieldPat, RecordLitField, Statement,
},
item_scope::BuiltinShadowMode,
item_tree::{ItemTree, ItemTreeId, ItemTreeNode},
path::{GenericArgs, Path},
type_ref::{Mutability, Rawness, TypeRef},
AdtId, ConstLoc, ContainerId, DefWithBodyId, EnumLoc, FunctionLoc, Intern, ModuleDefId,
StaticLoc, StructLoc, TraitLoc, TypeAliasLoc, UnionLoc,
};
use super::{ExprSource, PatSource};
pub(crate) struct LowerCtx {
hygiene: Hygiene,
}
impl LowerCtx {
pub fn new(db: &dyn DefDatabase, file_id: HirFileId) -> Self {
LowerCtx { hygiene: Hygiene::new(db.upcast(), file_id) }
}
pub fn with_hygiene(hygiene: &Hygiene) -> Self {
LowerCtx { hygiene: hygiene.clone() }
}
pub fn lower_path(&self, ast: ast::Path) -> Option<Path> {
Path::from_src(ast, &self.hygiene)
}
}
pub(super) fn lower(
db: &dyn DefDatabase,
def: DefWithBodyId,
expander: Expander,
params: Option<ast::ParamList>,
body: Option<ast::Expr>,
) -> (Body, BodySourceMap) {
let item_tree = db.item_tree(expander.current_file_id);
ExprCollector {
db,
def,
source_map: BodySourceMap::default(),
body: Body {
exprs: Arena::default(),
pats: Arena::default(),
params: Vec::new(),
body_expr: dummy_expr_id(),
item_scope: Default::default(),
},
item_trees: {
let mut map = FxHashMap::default();
map.insert(expander.current_file_id, item_tree);
map
},
expander,
}
.collect(params, body)
}
struct ExprCollector<'a> {
db: &'a dyn DefDatabase,
def: DefWithBodyId,
expander: Expander,
body: Body,
source_map: BodySourceMap,
item_trees: FxHashMap<HirFileId, Arc<ItemTree>>,
}
impl ExprCollector<'_> {
fn collect(
mut self,
param_list: Option<ast::ParamList>,
body: Option<ast::Expr>,
) -> (Body, BodySourceMap) {
if let Some(param_list) = param_list {
if let Some(self_param) = param_list.self_param() {
let ptr = AstPtr::new(&self_param);
let param_pat = self.alloc_pat(
Pat::Bind {
name: name![self],
mode: BindingAnnotation::Unannotated,
subpat: None,
},
Either::Right(ptr),
);
self.body.params.push(param_pat);
}
for param in param_list.params() {
let pat = match param.pat() {
None => continue,
Some(pat) => pat,
};
let param_pat = self.collect_pat(pat);
self.body.params.push(param_pat);
}
};
self.body.body_expr = self.collect_expr_opt(body);
(self.body, self.source_map)
}
fn ctx(&self) -> LowerCtx {
LowerCtx::new(self.db, self.expander.current_file_id)
}
fn alloc_expr(&mut self, expr: Expr, ptr: AstPtr<ast::Expr>) -> ExprId {
let src = self.expander.to_source(ptr);
let id = self.make_expr(expr, Ok(src.clone()));
self.source_map.expr_map.insert(src, id);
id
}
// desugared exprs don't have ptr, that's wrong and should be fixed
// somehow.
fn alloc_expr_desugared(&mut self, expr: Expr) -> ExprId {
self.make_expr(expr, Err(SyntheticSyntax))
}
fn empty_block(&mut self) -> ExprId {
self.alloc_expr_desugared(Expr::Block { statements: Vec::new(), tail: None, label: None })
}
fn missing_expr(&mut self) -> ExprId {
self.alloc_expr_desugared(Expr::Missing)
}
fn make_expr(&mut self, expr: Expr, src: Result<ExprSource, SyntheticSyntax>) -> ExprId {
let id = self.body.exprs.alloc(expr);
self.source_map.expr_map_back.insert(id, src);
id
}
fn alloc_pat(&mut self, pat: Pat, ptr: PatPtr) -> PatId {
let src = self.expander.to_source(ptr);
let id = self.make_pat(pat, Ok(src.clone()));
self.source_map.pat_map.insert(src, id);
id
}
fn missing_pat(&mut self) -> PatId {
self.make_pat(Pat::Missing, Err(SyntheticSyntax))
}
fn make_pat(&mut self, pat: Pat, src: Result<PatSource, SyntheticSyntax>) -> PatId {
let id = self.body.pats.alloc(pat);
self.source_map.pat_map_back.insert(id, src);
id
}
fn collect_expr(&mut self, expr: ast::Expr) -> ExprId {
let syntax_ptr = AstPtr::new(&expr);
if !self.expander.is_cfg_enabled(&expr) {
return self.missing_expr();
}
match expr {
ast::Expr::IfExpr(e) => {
let then_branch = self.collect_block_opt(e.then_branch());
let else_branch = e.else_branch().map(|b| match b {
ast::ElseBranch::Block(it) => self.collect_block(it),
ast::ElseBranch::IfExpr(elif) => {
let expr: ast::Expr = ast::Expr::cast(elif.syntax().clone()).unwrap();
self.collect_expr(expr)
}
});
let condition = match e.condition() {
None => self.missing_expr(),
Some(condition) => match condition.pat() {
None => self.collect_expr_opt(condition.expr()),
// if let -- desugar to match
Some(pat) => {
let pat = self.collect_pat(pat);
let match_expr = self.collect_expr_opt(condition.expr());
let placeholder_pat = self.missing_pat();
let arms = vec![
MatchArm { pat, expr: then_branch, guard: None },
MatchArm {
pat: placeholder_pat,
expr: else_branch.unwrap_or_else(|| self.empty_block()),
guard: None,
},
];
return self
.alloc_expr(Expr::Match { expr: match_expr, arms }, syntax_ptr);
}
},
};
self.alloc_expr(Expr::If { condition, then_branch, else_branch }, syntax_ptr)
}
ast::Expr::EffectExpr(e) => match e.effect() {
ast::Effect::Try(_) => {
let body = self.collect_block_opt(e.block_expr());
self.alloc_expr(Expr::TryBlock { body }, syntax_ptr)
}
ast::Effect::Unsafe(_) => {
let body = self.collect_block_opt(e.block_expr());
self.alloc_expr(Expr::Unsafe { body }, syntax_ptr)
}
// FIXME: we need to record these effects somewhere...
ast::Effect::Label(label) => match e.block_expr() {
Some(block) => {
let res = self.collect_block(block);
match &mut self.body.exprs[res] {
Expr::Block { label: block_label, .. } => {
*block_label =
label.lifetime_token().map(|t| Name::new_lifetime(&t))
}
_ => unreachable!(),
}
res
}
None => self.missing_expr(),
},
// FIXME: we need to record these effects somewhere...
ast::Effect::Async(_) => self.collect_block_opt(e.block_expr()),
},
ast::Expr::BlockExpr(e) => self.collect_block(e),
ast::Expr::LoopExpr(e) => {
let body = self.collect_block_opt(e.loop_body());
self.alloc_expr(
Expr::Loop {
body,
label: e
.label()
.and_then(|l| l.lifetime_token())
.map(|l| Name::new_lifetime(&l)),
},
syntax_ptr,
)
}
ast::Expr::WhileExpr(e) => {
let body = self.collect_block_opt(e.loop_body());
let condition = match e.condition() {
None => self.missing_expr(),
Some(condition) => match condition.pat() {
None => self.collect_expr_opt(condition.expr()),
// if let -- desugar to match
Some(pat) => {
mark::hit!(infer_resolve_while_let);
let pat = self.collect_pat(pat);
let match_expr = self.collect_expr_opt(condition.expr());
let placeholder_pat = self.missing_pat();
let break_ =
self.alloc_expr_desugared(Expr::Break { expr: None, label: None });
let arms = vec![
MatchArm { pat, expr: body, guard: None },
MatchArm { pat: placeholder_pat, expr: break_, guard: None },
];
let match_expr =
self.alloc_expr_desugared(Expr::Match { expr: match_expr, arms });
return self.alloc_expr(
Expr::Loop {
body: match_expr,
label: e
.label()
.and_then(|l| l.lifetime_token())
.map(|l| Name::new_lifetime(&l)),
},
syntax_ptr,
);
}
},
};
self.alloc_expr(
Expr::While {
condition,
body,
label: e
.label()
.and_then(|l| l.lifetime_token())
.map(|l| Name::new_lifetime(&l)),
},
syntax_ptr,
)
}
ast::Expr::ForExpr(e) => {
let iterable = self.collect_expr_opt(e.iterable());
let pat = self.collect_pat_opt(e.pat());
let body = self.collect_block_opt(e.loop_body());
self.alloc_expr(
Expr::For {
iterable,
pat,
body,
label: e
.label()
.and_then(|l| l.lifetime_token())
.map(|l| Name::new_lifetime(&l)),
},
syntax_ptr,
)
}
ast::Expr::CallExpr(e) => {
let callee = self.collect_expr_opt(e.expr());
let args = if let Some(arg_list) = e.arg_list() {
arg_list.args().map(|e| self.collect_expr(e)).collect()
} else {
Vec::new()
};
self.alloc_expr(Expr::Call { callee, args }, syntax_ptr)
}
ast::Expr::MethodCallExpr(e) => {
let receiver = self.collect_expr_opt(e.receiver());
let args = if let Some(arg_list) = e.arg_list() {
arg_list.args().map(|e| self.collect_expr(e)).collect()
} else {
Vec::new()
};
let method_name = e.name_ref().map(|nr| nr.as_name()).unwrap_or_else(Name::missing);
let generic_args =
e.generic_arg_list().and_then(|it| GenericArgs::from_ast(&self.ctx(), it));
self.alloc_expr(
Expr::MethodCall { receiver, method_name, args, generic_args },
syntax_ptr,
)
}
ast::Expr::MatchExpr(e) => {
let expr = self.collect_expr_opt(e.expr());
let arms = if let Some(match_arm_list) = e.match_arm_list() {
match_arm_list
.arms()
.map(|arm| MatchArm {
pat: self.collect_pat_opt(arm.pat()),
expr: self.collect_expr_opt(arm.expr()),
guard: arm
.guard()
.and_then(|guard| guard.expr())
.map(|e| self.collect_expr(e)),
})
.collect()
} else {
Vec::new()
};
self.alloc_expr(Expr::Match { expr, arms }, syntax_ptr)
}
ast::Expr::PathExpr(e) => {
let path = e
.path()
.and_then(|path| self.expander.parse_path(path))
.map(Expr::Path)
.unwrap_or(Expr::Missing);
self.alloc_expr(path, syntax_ptr)
}
ast::Expr::ContinueExpr(e) => self.alloc_expr(
Expr::Continue { label: e.lifetime_token().map(|l| Name::new_lifetime(&l)) },
syntax_ptr,
),
ast::Expr::BreakExpr(e) => {
let expr = e.expr().map(|e| self.collect_expr(e));
self.alloc_expr(
Expr::Break { expr, label: e.lifetime_token().map(|l| Name::new_lifetime(&l)) },
syntax_ptr,
)
}
ast::Expr::ParenExpr(e) => {
let inner = self.collect_expr_opt(e.expr());
// make the paren expr point to the inner expression as well
let src = self.expander.to_source(syntax_ptr);
self.source_map.expr_map.insert(src, inner);
inner
}
ast::Expr::ReturnExpr(e) => {
let expr = e.expr().map(|e| self.collect_expr(e));
self.alloc_expr(Expr::Return { expr }, syntax_ptr)
}
ast::Expr::RecordExpr(e) => {
let path = e.path().and_then(|path| self.expander.parse_path(path));
let mut field_ptrs = Vec::new();
let record_lit = if let Some(nfl) = e.record_expr_field_list() {
let fields = nfl
.fields()
.inspect(|field| field_ptrs.push(AstPtr::new(field)))
.filter_map(|field| {
if !self.expander.is_cfg_enabled(&field) {
return None;
}
let name = field.field_name()?.as_name();
Some(RecordLitField {
name,
expr: match field.expr() {
Some(e) => self.collect_expr(e),
None => self.missing_expr(),
},
})
})
.collect();
let spread = nfl.spread().map(|s| self.collect_expr(s));
Expr::RecordLit { path, fields, spread }
} else {
Expr::RecordLit { path, fields: Vec::new(), spread: None }
};
let res = self.alloc_expr(record_lit, syntax_ptr);
for (i, ptr) in field_ptrs.into_iter().enumerate() {
let src = self.expander.to_source(ptr);
self.source_map.field_map.insert((res, i), src);
}
res
}
ast::Expr::FieldExpr(e) => {
let expr = self.collect_expr_opt(e.expr());
let name = match e.field_access() {
Some(kind) => kind.as_name(),
_ => Name::missing(),
};
self.alloc_expr(Expr::Field { expr, name }, syntax_ptr)
}
ast::Expr::AwaitExpr(e) => {
let expr = self.collect_expr_opt(e.expr());
self.alloc_expr(Expr::Await { expr }, syntax_ptr)
}
ast::Expr::TryExpr(e) => {
let expr = self.collect_expr_opt(e.expr());
self.alloc_expr(Expr::Try { expr }, syntax_ptr)
}
ast::Expr::CastExpr(e) => {
let expr = self.collect_expr_opt(e.expr());
let type_ref = TypeRef::from_ast_opt(&self.ctx(), e.ty());
self.alloc_expr(Expr::Cast { expr, type_ref }, syntax_ptr)
}
ast::Expr::RefExpr(e) => {
let expr = self.collect_expr_opt(e.expr());
let raw_tok = e.raw_token().is_some();
let mutability = if raw_tok {
if e.mut_token().is_some() {
Mutability::Mut
} else if e.const_token().is_some() {
Mutability::Shared
} else {
unreachable!("parser only remaps to raw_token() if matching mutability token follows")
}
} else {
Mutability::from_mutable(e.mut_token().is_some())
};
let rawness = Rawness::from_raw(raw_tok);
self.alloc_expr(Expr::Ref { expr, rawness, mutability }, syntax_ptr)
}
ast::Expr::PrefixExpr(e) => {
let expr = self.collect_expr_opt(e.expr());
if let Some(op) = e.op_kind() {
self.alloc_expr(Expr::UnaryOp { expr, op }, syntax_ptr)
} else {
self.alloc_expr(Expr::Missing, syntax_ptr)
}
}
ast::Expr::ClosureExpr(e) => {
let mut args = Vec::new();
let mut arg_types = Vec::new();
if let Some(pl) = e.param_list() {
for param in pl.params() {
let pat = self.collect_pat_opt(param.pat());
let type_ref = param.ty().map(|it| TypeRef::from_ast(&self.ctx(), it));
args.push(pat);
arg_types.push(type_ref);
}
}
let ret_type =
e.ret_type().and_then(|r| r.ty()).map(|it| TypeRef::from_ast(&self.ctx(), it));
let body = self.collect_expr_opt(e.body());
self.alloc_expr(Expr::Lambda { args, arg_types, ret_type, body }, syntax_ptr)
}
ast::Expr::BinExpr(e) => {
let lhs = self.collect_expr_opt(e.lhs());
let rhs = self.collect_expr_opt(e.rhs());
let op = e.op_kind().map(BinaryOp::from);
self.alloc_expr(Expr::BinaryOp { lhs, rhs, op }, syntax_ptr)
}
ast::Expr::TupleExpr(e) => {
let exprs = e.fields().map(|expr| self.collect_expr(expr)).collect();
self.alloc_expr(Expr::Tuple { exprs }, syntax_ptr)
}
ast::Expr::BoxExpr(e) => {
let expr = self.collect_expr_opt(e.expr());
self.alloc_expr(Expr::Box { expr }, syntax_ptr)
}
ast::Expr::ArrayExpr(e) => {
let kind = e.kind();
match kind {
ArrayExprKind::ElementList(e) => {
let exprs = e.map(|expr| self.collect_expr(expr)).collect();
self.alloc_expr(Expr::Array(Array::ElementList(exprs)), syntax_ptr)
}
ArrayExprKind::Repeat { initializer, repeat } => {
let initializer = self.collect_expr_opt(initializer);
let repeat = self.collect_expr_opt(repeat);
self.alloc_expr(
Expr::Array(Array::Repeat { initializer, repeat }),
syntax_ptr,
)
}
}
}
ast::Expr::Literal(e) => self.alloc_expr(Expr::Literal(e.kind().into()), syntax_ptr),
ast::Expr::IndexExpr(e) => {
let base = self.collect_expr_opt(e.base());
let index = self.collect_expr_opt(e.index());
self.alloc_expr(Expr::Index { base, index }, syntax_ptr)
}
ast::Expr::RangeExpr(e) => {
let lhs = e.start().map(|lhs| self.collect_expr(lhs));
let rhs = e.end().map(|rhs| self.collect_expr(rhs));
match e.op_kind() {
Some(range_type) => {
self.alloc_expr(Expr::Range { lhs, rhs, range_type }, syntax_ptr)
}
None => self.alloc_expr(Expr::Missing, syntax_ptr),
}
}
ast::Expr::MacroCall(e) => {
if let Some(name) = e.is_macro_rules().map(|it| it.as_name()) {
let mac = MacroDefId {
krate: Some(self.expander.module.krate),
ast_id: Some(self.expander.ast_id(&e)),
kind: MacroDefKind::Declarative,
local_inner: false,
};
self.body.item_scope.define_legacy_macro(name, mac);
// FIXME: do we still need to allocate this as missing ?
self.alloc_expr(Expr::Missing, syntax_ptr)
} else {
let macro_call = self.expander.to_source(AstPtr::new(&e));
match self.expander.enter_expand(self.db, Some(&self.body.item_scope), e) {
Some((mark, expansion)) => {
self.source_map
.expansions
.insert(macro_call, self.expander.current_file_id);
let item_tree = self.db.item_tree(self.expander.current_file_id);
self.item_trees.insert(self.expander.current_file_id, item_tree);
let id = self.collect_expr(expansion);
self.expander.exit(self.db, mark);
id
}
None => self.alloc_expr(Expr::Missing, syntax_ptr),
}
}
}
}
}
fn find_inner_item<N: ItemTreeNode>(&self, ast: &N::Source) -> Option<ItemTreeId<N>> {
let id = self.expander.ast_id(ast);
let tree = &self.item_trees[&id.file_id];
// FIXME: This probably breaks with `use` items, since they produce multiple item tree nodes
// Root file (non-macro).
let item_tree_id = tree
.all_inner_items()
.chain(tree.top_level_items().iter().copied())
.filter_map(|mod_item| mod_item.downcast::<N>())
.find(|tree_id| tree[*tree_id].ast_id().upcast() == id.value.upcast())
.or_else(|| {
log::debug!(
"couldn't find inner {} item for {:?} (AST: `{}` - {:?})",
type_name::<N>(),
id,
ast.syntax(),
ast.syntax(),
);
None
})?;
Some(ItemTreeId::new(id.file_id, item_tree_id))
}
fn collect_expr_opt(&mut self, expr: Option<ast::Expr>) -> ExprId {
if let Some(expr) = expr {
self.collect_expr(expr)
} else {
self.missing_expr()
}
}
fn collect_block(&mut self, block: ast::BlockExpr) -> ExprId {
let syntax_node_ptr = AstPtr::new(&block.clone().into());
self.collect_block_items(&block);
let statements = block
.statements()
.filter_map(|s| {
let stmt = match s {
ast::Stmt::LetStmt(stmt) => {
let pat = self.collect_pat_opt(stmt.pat());
let type_ref = stmt.ty().map(|it| TypeRef::from_ast(&self.ctx(), it));
let initializer = stmt.initializer().map(|e| self.collect_expr(e));
Statement::Let { pat, type_ref, initializer }
}
ast::Stmt::ExprStmt(stmt) => {
Statement::Expr(self.collect_expr_opt(stmt.expr()))
}
ast::Stmt::Item(_) => return None,
};
Some(stmt)
})
.collect();
let tail = block.expr().map(|e| self.collect_expr(e));
self.alloc_expr(Expr::Block { statements, tail, label: None }, syntax_node_ptr)
}
fn collect_block_items(&mut self, block: &ast::BlockExpr) {
let container = ContainerId::DefWithBodyId(self.def);
let items = block
.statements()
.filter_map(|stmt| match stmt {
ast::Stmt::Item(it) => Some(it),
ast::Stmt::LetStmt(_) | ast::Stmt::ExprStmt(_) => None,
})
.filter_map(|item| {
let (def, name): (ModuleDefId, Option<ast::Name>) = match item {
ast::Item::Fn(def) => {
let id = self.find_inner_item(&def)?;
(
FunctionLoc { container: container.into(), id }.intern(self.db).into(),
def.name(),
)
}
ast::Item::TypeAlias(def) => {
let id = self.find_inner_item(&def)?;
(
TypeAliasLoc { container: container.into(), id }.intern(self.db).into(),
def.name(),
)
}
ast::Item::Const(def) => {
let id = self.find_inner_item(&def)?;
(
ConstLoc { container: container.into(), id }.intern(self.db).into(),
def.name(),
)
}
ast::Item::Static(def) => {
let id = self.find_inner_item(&def)?;
(StaticLoc { container, id }.intern(self.db).into(), def.name())
}
ast::Item::Struct(def) => {
let id = self.find_inner_item(&def)?;
(StructLoc { container, id }.intern(self.db).into(), def.name())
}
ast::Item::Enum(def) => {
let id = self.find_inner_item(&def)?;
(EnumLoc { container, id }.intern(self.db).into(), def.name())
}
ast::Item::Union(def) => {
let id = self.find_inner_item(&def)?;
(UnionLoc { container, id }.intern(self.db).into(), def.name())
}
ast::Item::Trait(def) => {
let id = self.find_inner_item(&def)?;
(TraitLoc { container, id }.intern(self.db).into(), def.name())
}
ast::Item::ExternBlock(_) => return None, // FIXME: collect from extern blocks
ast::Item::Impl(_)
| ast::Item::Use(_)
| ast::Item::ExternCrate(_)
| ast::Item::Module(_)
| ast::Item::MacroCall(_) => return None,
};
Some((def, name))
})
.collect::<Vec<_>>();
for (def, name) in items {
self.body.item_scope.define_def(def);
if let Some(name) = name {
let vis = crate::visibility::Visibility::Public; // FIXME determine correctly
let has_constructor = match def {
ModuleDefId::AdtId(AdtId::StructId(s)) => {
self.db.struct_data(s).variant_data.kind() != StructKind::Record
}
_ => true,
};
self.body.item_scope.push_res(
name.as_name(),
crate::per_ns::PerNs::from_def(def, vis, has_constructor),
);
}
}
}
fn collect_block_opt(&mut self, expr: Option<ast::BlockExpr>) -> ExprId {
if let Some(block) = expr {
self.collect_block(block)
} else {
self.missing_expr()
}
}
fn collect_pat(&mut self, pat: ast::Pat) -> PatId {
let pattern = match &pat {
ast::Pat::IdentPat(bp) => {
let name = bp.name().map(|nr| nr.as_name()).unwrap_or_else(Name::missing);
let annotation =
BindingAnnotation::new(bp.mut_token().is_some(), bp.ref_token().is_some());
let subpat = bp.pat().map(|subpat| self.collect_pat(subpat));
if annotation == BindingAnnotation::Unannotated && subpat.is_none() {
// This could also be a single-segment path pattern. To
// decide that, we need to try resolving the name.
let (resolved, _) = self.expander.crate_def_map.resolve_path(
self.db,
self.expander.module.local_id,
&name.clone().into(),
BuiltinShadowMode::Other,
);
match resolved.take_values() {
Some(ModuleDefId::ConstId(_)) => Pat::Path(name.into()),
Some(ModuleDefId::EnumVariantId(_)) => {
// this is only really valid for unit variants, but
// shadowing other enum variants with a pattern is
// an error anyway
Pat::Path(name.into())
}
Some(ModuleDefId::AdtId(AdtId::StructId(s)))
if self.db.struct_data(s).variant_data.kind() != StructKind::Record =>
{
// Funnily enough, record structs *can* be shadowed
// by pattern bindings (but unit or tuple structs
// can't).
Pat::Path(name.into())
}
// shadowing statics is an error as well, so we just ignore that case here
_ => Pat::Bind { name, mode: annotation, subpat },
}
} else {
Pat::Bind { name, mode: annotation, subpat }
}
}
ast::Pat::TupleStructPat(p) => {
let path = p.path().and_then(|path| self.expander.parse_path(path));
let (args, ellipsis) = self.collect_tuple_pat(p.fields());
Pat::TupleStruct { path, args, ellipsis }
}
ast::Pat::RefPat(p) => {
let pat = self.collect_pat_opt(p.pat());
let mutability = Mutability::from_mutable(p.mut_token().is_some());
Pat::Ref { pat, mutability }
}
ast::Pat::PathPat(p) => {
let path = p.path().and_then(|path| self.expander.parse_path(path));
path.map(Pat::Path).unwrap_or(Pat::Missing)
}
ast::Pat::OrPat(p) => {
let pats = p.pats().map(|p| self.collect_pat(p)).collect();
Pat::Or(pats)
}
ast::Pat::ParenPat(p) => return self.collect_pat_opt(p.pat()),
ast::Pat::TuplePat(p) => {
let (args, ellipsis) = self.collect_tuple_pat(p.fields());
Pat::Tuple { args, ellipsis }
}
ast::Pat::WildcardPat(_) => Pat::Wild,
ast::Pat::RecordPat(p) => {
let path = p.path().and_then(|path| self.expander.parse_path(path));
let args: Vec<_> = p
.record_pat_field_list()
.expect("every struct should have a field list")
.fields()
.filter_map(|f| {
let ast_pat = f.pat()?;
let pat = self.collect_pat(ast_pat);
let name = f.field_name()?.as_name();
Some(RecordFieldPat { name, pat })
})
.collect();
let ellipsis = p
.record_pat_field_list()
.expect("every struct should have a field list")
.dotdot_token()
.is_some();
Pat::Record { path, args, ellipsis }
}
ast::Pat::SlicePat(p) => {
let SlicePatComponents { prefix, slice, suffix } = p.components();
// FIXME properly handle `RestPat`
Pat::Slice {
prefix: prefix.into_iter().map(|p| self.collect_pat(p)).collect(),
slice: slice.map(|p| self.collect_pat(p)),
suffix: suffix.into_iter().map(|p| self.collect_pat(p)).collect(),
}
}
ast::Pat::LiteralPat(lit) => {
if let Some(ast_lit) = lit.literal() {
let expr = Expr::Literal(ast_lit.kind().into());
let expr_ptr = AstPtr::new(&ast::Expr::Literal(ast_lit));
let expr_id = self.alloc_expr(expr, expr_ptr);
Pat::Lit(expr_id)
} else {
Pat::Missing
}
}
ast::Pat::RestPat(_) => {
// `RestPat` requires special handling and should not be mapped
// to a Pat. Here we are using `Pat::Missing` as a fallback for
// when `RestPat` is mapped to `Pat`, which can easily happen
// when the source code being analyzed has a malformed pattern
// which includes `..` in a place where it isn't valid.
Pat::Missing
}
// FIXME: implement
ast::Pat::BoxPat(_) | ast::Pat::RangePat(_) | ast::Pat::MacroPat(_) => Pat::Missing,
};
let ptr = AstPtr::new(&pat);
self.alloc_pat(pattern, Either::Left(ptr))
}
fn collect_pat_opt(&mut self, pat: Option<ast::Pat>) -> PatId {
if let Some(pat) = pat {
self.collect_pat(pat)
} else {
self.missing_pat()
}
}
fn collect_tuple_pat(&mut self, args: AstChildren<ast::Pat>) -> (Vec<PatId>, Option<usize>) {
// Find the location of the `..`, if there is one. Note that we do not
// consider the possiblity of there being multiple `..` here.
let ellipsis = args.clone().position(|p| matches!(p, ast::Pat::RestPat(_)));
// We want to skip the `..` pattern here, since we account for it above.
let args = args
.filter(|p| !matches!(p, ast::Pat::RestPat(_)))
.map(|p| self.collect_pat(p))
.collect();
(args, ellipsis)
}
}
impl From<ast::BinOp> for BinaryOp {
fn from(ast_op: ast::BinOp) -> Self {
match ast_op {
ast::BinOp::BooleanOr => BinaryOp::LogicOp(LogicOp::Or),
ast::BinOp::BooleanAnd => BinaryOp::LogicOp(LogicOp::And),
ast::BinOp::EqualityTest => BinaryOp::CmpOp(CmpOp::Eq { negated: false }),
ast::BinOp::NegatedEqualityTest => BinaryOp::CmpOp(CmpOp::Eq { negated: true }),
ast::BinOp::LesserEqualTest => {
BinaryOp::CmpOp(CmpOp::Ord { ordering: Ordering::Less, strict: false })
}
ast::BinOp::GreaterEqualTest => {
BinaryOp::CmpOp(CmpOp::Ord { ordering: Ordering::Greater, strict: false })
}
ast::BinOp::LesserTest => {
BinaryOp::CmpOp(CmpOp::Ord { ordering: Ordering::Less, strict: true })
}
ast::BinOp::GreaterTest => {
BinaryOp::CmpOp(CmpOp::Ord { ordering: Ordering::Greater, strict: true })
}
ast::BinOp::Addition => BinaryOp::ArithOp(ArithOp::Add),
ast::BinOp::Multiplication => BinaryOp::ArithOp(ArithOp::Mul),
ast::BinOp::Subtraction => BinaryOp::ArithOp(ArithOp::Sub),
ast::BinOp::Division => BinaryOp::ArithOp(ArithOp::Div),
ast::BinOp::Remainder => BinaryOp::ArithOp(ArithOp::Rem),
ast::BinOp::LeftShift => BinaryOp::ArithOp(ArithOp::Shl),
ast::BinOp::RightShift => BinaryOp::ArithOp(ArithOp::Shr),
ast::BinOp::BitwiseXor => BinaryOp::ArithOp(ArithOp::BitXor),
ast::BinOp::BitwiseOr => BinaryOp::ArithOp(ArithOp::BitOr),
ast::BinOp::BitwiseAnd => BinaryOp::ArithOp(ArithOp::BitAnd),
ast::BinOp::Assignment => BinaryOp::Assignment { op: None },
ast::BinOp::AddAssign => BinaryOp::Assignment { op: Some(ArithOp::Add) },
ast::BinOp::DivAssign => BinaryOp::Assignment { op: Some(ArithOp::Div) },
ast::BinOp::MulAssign => BinaryOp::Assignment { op: Some(ArithOp::Mul) },
ast::BinOp::RemAssign => BinaryOp::Assignment { op: Some(ArithOp::Rem) },
ast::BinOp::ShlAssign => BinaryOp::Assignment { op: Some(ArithOp::Shl) },
ast::BinOp::ShrAssign => BinaryOp::Assignment { op: Some(ArithOp::Shr) },
ast::BinOp::SubAssign => BinaryOp::Assignment { op: Some(ArithOp::Sub) },
ast::BinOp::BitOrAssign => BinaryOp::Assignment { op: Some(ArithOp::BitOr) },
ast::BinOp::BitAndAssign => BinaryOp::Assignment { op: Some(ArithOp::BitAnd) },
ast::BinOp::BitXorAssign => BinaryOp::Assignment { op: Some(ArithOp::BitXor) },
}
}
}
impl From<ast::LiteralKind> for Literal {
fn from(ast_lit_kind: ast::LiteralKind) -> Self {
match ast_lit_kind {
LiteralKind::IntNumber { suffix } => {
let known_name = suffix.and_then(|it| BuiltinInt::from_suffix(&it));
Literal::Int(Default::default(), known_name)
}
LiteralKind::FloatNumber { suffix } => {
let known_name = suffix.and_then(|it| BuiltinFloat::from_suffix(&it));
Literal::Float(Default::default(), known_name)
}
LiteralKind::ByteString => Literal::ByteString(Default::default()),
LiteralKind::String => Literal::String(Default::default()),
LiteralKind::Byte => Literal::Int(Default::default(), Some(BuiltinInt::U8)),
LiteralKind::Bool(val) => Literal::Bool(val),
LiteralKind::Char => Literal::Char(Default::default()),
}
}
}

View file

@ -0,0 +1,456 @@
//! Name resolution for expressions.
use std::sync::Arc;
use arena::{Arena, Idx};
use hir_expand::name::Name;
use rustc_hash::FxHashMap;
use crate::{
body::Body,
db::DefDatabase,
expr::{Expr, ExprId, Pat, PatId, Statement},
DefWithBodyId,
};
pub type ScopeId = Idx<ScopeData>;
#[derive(Debug, PartialEq, Eq)]
pub struct ExprScopes {
scopes: Arena<ScopeData>,
scope_by_expr: FxHashMap<ExprId, ScopeId>,
}
#[derive(Debug, PartialEq, Eq)]
pub struct ScopeEntry {
name: Name,
pat: PatId,
}
impl ScopeEntry {
pub fn name(&self) -> &Name {
&self.name
}
pub fn pat(&self) -> PatId {
self.pat
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct ScopeData {
parent: Option<ScopeId>,
entries: Vec<ScopeEntry>,
}
impl ExprScopes {
pub(crate) fn expr_scopes_query(db: &dyn DefDatabase, def: DefWithBodyId) -> Arc<ExprScopes> {
let body = db.body(def);
Arc::new(ExprScopes::new(&*body))
}
fn new(body: &Body) -> ExprScopes {
let mut scopes =
ExprScopes { scopes: Arena::default(), scope_by_expr: FxHashMap::default() };
let root = scopes.root_scope();
scopes.add_params_bindings(body, root, &body.params);
compute_expr_scopes(body.body_expr, body, &mut scopes, root);
scopes
}
pub fn entries(&self, scope: ScopeId) -> &[ScopeEntry] {
&self.scopes[scope].entries
}
pub fn scope_chain(&self, scope: Option<ScopeId>) -> impl Iterator<Item = ScopeId> + '_ {
std::iter::successors(scope, move |&scope| self.scopes[scope].parent)
}
pub fn resolve_name_in_scope(&self, scope: ScopeId, name: &Name) -> Option<&ScopeEntry> {
self.scope_chain(Some(scope))
.find_map(|scope| self.entries(scope).iter().find(|it| it.name == *name))
}
pub fn scope_for(&self, expr: ExprId) -> Option<ScopeId> {
self.scope_by_expr.get(&expr).copied()
}
pub fn scope_by_expr(&self) -> &FxHashMap<ExprId, ScopeId> {
&self.scope_by_expr
}
fn root_scope(&mut self) -> ScopeId {
self.scopes.alloc(ScopeData { parent: None, entries: vec![] })
}
fn new_scope(&mut self, parent: ScopeId) -> ScopeId {
self.scopes.alloc(ScopeData { parent: Some(parent), entries: vec![] })
}
fn add_bindings(&mut self, body: &Body, scope: ScopeId, pat: PatId) {
let pattern = &body[pat];
if let Pat::Bind { name, .. } = pattern {
let entry = ScopeEntry { name: name.clone(), pat };
self.scopes[scope].entries.push(entry);
}
pattern.walk_child_pats(|pat| self.add_bindings(body, scope, pat));
}
fn add_params_bindings(&mut self, body: &Body, scope: ScopeId, params: &[PatId]) {
params.iter().for_each(|pat| self.add_bindings(body, scope, *pat));
}
fn set_scope(&mut self, node: ExprId, scope: ScopeId) {
self.scope_by_expr.insert(node, scope);
}
}
fn compute_block_scopes(
statements: &[Statement],
tail: Option<ExprId>,
body: &Body,
scopes: &mut ExprScopes,
mut scope: ScopeId,
) {
for stmt in statements {
match stmt {
Statement::Let { pat, initializer, .. } => {
if let Some(expr) = initializer {
scopes.set_scope(*expr, scope);
compute_expr_scopes(*expr, body, scopes, scope);
}
scope = scopes.new_scope(scope);
scopes.add_bindings(body, scope, *pat);
}
Statement::Expr(expr) => {
scopes.set_scope(*expr, scope);
compute_expr_scopes(*expr, body, scopes, scope);
}
}
}
if let Some(expr) = tail {
compute_expr_scopes(expr, body, scopes, scope);
}
}
fn compute_expr_scopes(expr: ExprId, body: &Body, scopes: &mut ExprScopes, scope: ScopeId) {
scopes.set_scope(expr, scope);
match &body[expr] {
Expr::Block { statements, tail, .. } => {
compute_block_scopes(&statements, *tail, body, scopes, scope);
}
Expr::For { iterable, pat, body: body_expr, .. } => {
compute_expr_scopes(*iterable, body, scopes, scope);
let scope = scopes.new_scope(scope);
scopes.add_bindings(body, scope, *pat);
compute_expr_scopes(*body_expr, body, scopes, scope);
}
Expr::Lambda { args, body: body_expr, .. } => {
let scope = scopes.new_scope(scope);
scopes.add_params_bindings(body, scope, &args);
compute_expr_scopes(*body_expr, body, scopes, scope);
}
Expr::Match { expr, arms } => {
compute_expr_scopes(*expr, body, scopes, scope);
for arm in arms {
let scope = scopes.new_scope(scope);
scopes.add_bindings(body, scope, arm.pat);
if let Some(guard) = arm.guard {
scopes.set_scope(guard, scope);
compute_expr_scopes(guard, body, scopes, scope);
}
scopes.set_scope(arm.expr, scope);
compute_expr_scopes(arm.expr, body, scopes, scope);
}
}
e => e.walk_child_exprs(|e| compute_expr_scopes(e, body, scopes, scope)),
};
}
#[cfg(test)]
mod tests {
use base_db::{fixture::WithFixture, FileId, SourceDatabase};
use hir_expand::{name::AsName, InFile};
use syntax::{algo::find_node_at_offset, ast, AstNode};
use test_utils::{assert_eq_text, extract_offset, mark};
use crate::{db::DefDatabase, test_db::TestDB, FunctionId, ModuleDefId};
fn find_function(db: &TestDB, file_id: FileId) -> FunctionId {
let krate = db.test_crate();
let crate_def_map = db.crate_def_map(krate);
let module = crate_def_map.modules_for_file(file_id).next().unwrap();
let (_, def) = crate_def_map[module].scope.entries().next().unwrap();
match def.take_values().unwrap() {
ModuleDefId::FunctionId(it) => it,
_ => panic!(),
}
}
fn do_check(ra_fixture: &str, expected: &[&str]) {
let (offset, code) = extract_offset(ra_fixture);
let code = {
let mut buf = String::new();
let off: usize = offset.into();
buf.push_str(&code[..off]);
buf.push_str("<|>marker");
buf.push_str(&code[off..]);
buf
};
let (db, position) = TestDB::with_position(&code);
let file_id = position.file_id;
let offset = position.offset;
let file_syntax = db.parse(file_id).syntax_node();
let marker: ast::PathExpr = find_node_at_offset(&file_syntax, offset).unwrap();
let function = find_function(&db, file_id);
let scopes = db.expr_scopes(function.into());
let (_body, source_map) = db.body_with_source_map(function.into());
let expr_id = source_map
.node_expr(InFile { file_id: file_id.into(), value: &marker.into() })
.unwrap();
let scope = scopes.scope_for(expr_id);
let actual = scopes
.scope_chain(scope)
.flat_map(|scope| scopes.entries(scope))
.map(|it| it.name().to_string())
.collect::<Vec<_>>()
.join("\n");
let expected = expected.join("\n");
assert_eq_text!(&expected, &actual);
}
#[test]
fn test_lambda_scope() {
do_check(
r"
fn quux(foo: i32) {
let f = |bar, baz: i32| {
<|>
};
}",
&["bar", "baz", "foo"],
);
}
#[test]
fn test_call_scope() {
do_check(
r"
fn quux() {
f(|x| <|> );
}",
&["x"],
);
}
#[test]
fn test_method_call_scope() {
do_check(
r"
fn quux() {
z.f(|x| <|> );
}",
&["x"],
);
}
#[test]
fn test_loop_scope() {
do_check(
r"
fn quux() {
loop {
let x = ();
<|>
};
}",
&["x"],
);
}
#[test]
fn test_match() {
do_check(
r"
fn quux() {
match () {
Some(x) => {
<|>
}
};
}",
&["x"],
);
}
#[test]
fn test_shadow_variable() {
do_check(
r"
fn foo(x: String) {
let x : &str = &x<|>;
}",
&["x"],
);
}
#[test]
fn test_bindings_after_at() {
do_check(
r"
fn foo() {
match Some(()) {
opt @ Some(unit) => {
<|>
}
_ => {}
}
}
",
&["opt", "unit"],
);
}
#[test]
fn macro_inner_item() {
do_check(
r"
macro_rules! mac {
() => {{
fn inner() {}
inner();
}};
}
fn foo() {
mac!();
<|>
}
",
&[],
);
}
#[test]
fn broken_inner_item() {
do_check(
r"
fn foo() {
trait {}
<|>
}
",
&[],
);
}
fn do_check_local_name(ra_fixture: &str, expected_offset: u32) {
let (db, position) = TestDB::with_position(ra_fixture);
let file_id = position.file_id;
let offset = position.offset;
let file = db.parse(file_id).ok().unwrap();
let expected_name = find_node_at_offset::<ast::Name>(file.syntax(), expected_offset.into())
.expect("failed to find a name at the target offset");
let name_ref: ast::NameRef = find_node_at_offset(file.syntax(), offset).unwrap();
let function = find_function(&db, file_id);
let scopes = db.expr_scopes(function.into());
let (_body, source_map) = db.body_with_source_map(function.into());
let expr_scope = {
let expr_ast = name_ref.syntax().ancestors().find_map(ast::Expr::cast).unwrap();
let expr_id =
source_map.node_expr(InFile { file_id: file_id.into(), value: &expr_ast }).unwrap();
scopes.scope_for(expr_id).unwrap()
};
let resolved = scopes.resolve_name_in_scope(expr_scope, &name_ref.as_name()).unwrap();
let pat_src = source_map.pat_syntax(resolved.pat()).unwrap();
let local_name = pat_src.value.either(
|it| it.syntax_node_ptr().to_node(file.syntax()),
|it| it.syntax_node_ptr().to_node(file.syntax()),
);
assert_eq!(local_name.text_range(), expected_name.syntax().text_range());
}
#[test]
fn test_resolve_local_name() {
do_check_local_name(
r#"
fn foo(x: i32, y: u32) {
{
let z = x * 2;
}
{
let t = x<|> * 3;
}
}
"#,
7,
);
}
#[test]
fn test_resolve_local_name_declaration() {
do_check_local_name(
r#"
fn foo(x: String) {
let x : &str = &x<|>;
}
"#,
7,
);
}
#[test]
fn test_resolve_local_name_shadow() {
do_check_local_name(
r"
fn foo(x: String) {
let x : &str = &x;
x<|>
}
",
28,
);
}
#[test]
fn ref_patterns_contribute_bindings() {
do_check_local_name(
r"
fn foo() {
if let Some(&from) = bar() {
from<|>;
}
}
",
28,
);
}
#[test]
fn while_let_desugaring() {
mark::check!(infer_resolve_while_let);
do_check_local_name(
r#"
fn test() {
let foo: Option<f32> = None;
while let Option::Some(spam) = foo {
spam<|>
}
}
"#,
75,
);
}
}

278
crates/hir_def/src/data.rs Normal file
View file

@ -0,0 +1,278 @@
//! Contains basic data about various HIR declarations.
use std::sync::Arc;
use hir_expand::{name::Name, InFile};
use syntax::ast;
use crate::{
attr::Attrs,
body::Expander,
db::DefDatabase,
item_tree::{AssocItem, ItemTreeId, ModItem},
type_ref::{TypeBound, TypeRef},
visibility::RawVisibility,
AssocContainerId, AssocItemId, ConstId, ConstLoc, FunctionId, FunctionLoc, HasModule, ImplId,
Intern, Lookup, ModuleId, StaticId, TraitId, TypeAliasId, TypeAliasLoc,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FunctionData {
pub name: Name,
pub params: Vec<TypeRef>,
pub ret_type: TypeRef,
pub attrs: Attrs,
/// True if the first param is `self`. This is relevant to decide whether this
/// can be called as a method.
pub has_self_param: bool,
pub is_unsafe: bool,
pub is_varargs: bool,
pub visibility: RawVisibility,
}
impl FunctionData {
pub(crate) fn fn_data_query(db: &dyn DefDatabase, func: FunctionId) -> Arc<FunctionData> {
let loc = func.lookup(db);
let item_tree = db.item_tree(loc.id.file_id);
let func = &item_tree[loc.id.value];
Arc::new(FunctionData {
name: func.name.clone(),
params: func.params.to_vec(),
ret_type: func.ret_type.clone(),
attrs: item_tree.attrs(ModItem::from(loc.id.value).into()).clone(),
has_self_param: func.has_self_param,
is_unsafe: func.is_unsafe,
is_varargs: func.is_varargs,
visibility: item_tree[func.visibility].clone(),
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TypeAliasData {
pub name: Name,
pub type_ref: Option<TypeRef>,
pub visibility: RawVisibility,
/// Bounds restricting the type alias itself (eg. `type Ty: Bound;` in a trait or impl).
pub bounds: Vec<TypeBound>,
}
impl TypeAliasData {
pub(crate) fn type_alias_data_query(
db: &dyn DefDatabase,
typ: TypeAliasId,
) -> Arc<TypeAliasData> {
let loc = typ.lookup(db);
let item_tree = db.item_tree(loc.id.file_id);
let typ = &item_tree[loc.id.value];
Arc::new(TypeAliasData {
name: typ.name.clone(),
type_ref: typ.type_ref.clone(),
visibility: item_tree[typ.visibility].clone(),
bounds: typ.bounds.to_vec(),
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TraitData {
pub name: Name,
pub items: Vec<(Name, AssocItemId)>,
pub auto: bool,
}
impl TraitData {
pub(crate) fn trait_data_query(db: &dyn DefDatabase, tr: TraitId) -> Arc<TraitData> {
let tr_loc = tr.lookup(db);
let item_tree = db.item_tree(tr_loc.id.file_id);
let tr_def = &item_tree[tr_loc.id.value];
let name = tr_def.name.clone();
let auto = tr_def.auto;
let module_id = tr_loc.container.module(db);
let container = AssocContainerId::TraitId(tr);
let mut expander = Expander::new(db, tr_loc.id.file_id, module_id);
let items = collect_items(
db,
module_id,
&mut expander,
tr_def.items.iter().copied(),
tr_loc.id.file_id,
container,
100,
);
Arc::new(TraitData { name, items, auto })
}
pub fn associated_types(&self) -> impl Iterator<Item = TypeAliasId> + '_ {
self.items.iter().filter_map(|(_name, item)| match item {
AssocItemId::TypeAliasId(t) => Some(*t),
_ => None,
})
}
pub fn associated_type_by_name(&self, name: &Name) -> Option<TypeAliasId> {
self.items.iter().find_map(|(item_name, item)| match item {
AssocItemId::TypeAliasId(t) if item_name == name => Some(*t),
_ => None,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ImplData {
pub target_trait: Option<TypeRef>,
pub target_type: TypeRef,
pub items: Vec<AssocItemId>,
pub is_negative: bool,
}
impl ImplData {
pub(crate) fn impl_data_query(db: &dyn DefDatabase, id: ImplId) -> Arc<ImplData> {
let _p = profile::span("impl_data_query");
let impl_loc = id.lookup(db);
let item_tree = db.item_tree(impl_loc.id.file_id);
let impl_def = &item_tree[impl_loc.id.value];
let target_trait = impl_def.target_trait.clone();
let target_type = impl_def.target_type.clone();
let is_negative = impl_def.is_negative;
let module_id = impl_loc.container.module(db);
let container = AssocContainerId::ImplId(id);
let mut expander = Expander::new(db, impl_loc.id.file_id, module_id);
let items = collect_items(
db,
module_id,
&mut expander,
impl_def.items.iter().copied(),
impl_loc.id.file_id,
container,
100,
);
let items = items.into_iter().map(|(_, item)| item).collect();
Arc::new(ImplData { target_trait, target_type, items, is_negative })
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConstData {
/// const _: () = ();
pub name: Option<Name>,
pub type_ref: TypeRef,
pub visibility: RawVisibility,
}
impl ConstData {
pub(crate) fn const_data_query(db: &dyn DefDatabase, konst: ConstId) -> Arc<ConstData> {
let loc = konst.lookup(db);
let item_tree = db.item_tree(loc.id.file_id);
let konst = &item_tree[loc.id.value];
Arc::new(ConstData {
name: konst.name.clone(),
type_ref: konst.type_ref.clone(),
visibility: item_tree[konst.visibility].clone(),
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StaticData {
pub name: Option<Name>,
pub type_ref: TypeRef,
pub visibility: RawVisibility,
pub mutable: bool,
}
impl StaticData {
pub(crate) fn static_data_query(db: &dyn DefDatabase, konst: StaticId) -> Arc<StaticData> {
let node = konst.lookup(db);
let item_tree = db.item_tree(node.id.file_id);
let statik = &item_tree[node.id.value];
Arc::new(StaticData {
name: Some(statik.name.clone()),
type_ref: statik.type_ref.clone(),
visibility: item_tree[statik.visibility].clone(),
mutable: statik.mutable,
})
}
}
fn collect_items(
db: &dyn DefDatabase,
module: ModuleId,
expander: &mut Expander,
assoc_items: impl Iterator<Item = AssocItem>,
file_id: crate::HirFileId,
container: AssocContainerId,
limit: usize,
) -> Vec<(Name, AssocItemId)> {
if limit == 0 {
return Vec::new();
}
let item_tree = db.item_tree(file_id);
let cfg_options = db.crate_graph()[module.krate].cfg_options.clone();
let mut items = Vec::new();
for item in assoc_items {
match item {
AssocItem::Function(id) => {
let item = &item_tree[id];
let attrs = item_tree.attrs(ModItem::from(id).into());
if !attrs.is_cfg_enabled(&cfg_options) {
continue;
}
let def = FunctionLoc { container, id: ItemTreeId::new(file_id, id) }.intern(db);
items.push((item.name.clone(), def.into()));
}
// FIXME: cfg?
AssocItem::Const(id) => {
let item = &item_tree[id];
let name = match item.name.clone() {
Some(name) => name,
None => continue,
};
let def = ConstLoc { container, id: ItemTreeId::new(file_id, id) }.intern(db);
items.push((name, def.into()));
}
AssocItem::TypeAlias(id) => {
let item = &item_tree[id];
let def = TypeAliasLoc { container, id: ItemTreeId::new(file_id, id) }.intern(db);
items.push((item.name.clone(), def.into()));
}
AssocItem::MacroCall(call) => {
let call = &item_tree[call];
let ast_id_map = db.ast_id_map(file_id);
let root = db.parse_or_expand(file_id).unwrap();
let call = ast_id_map.get(call.ast_id).to_node(&root);
if let Some((mark, mac)) = expander.enter_expand(db, None, call) {
let src: InFile<ast::MacroItems> = expander.to_source(mac);
let item_tree = db.item_tree(src.file_id);
let iter =
item_tree.top_level_items().iter().filter_map(ModItem::as_assoc_item);
items.extend(collect_items(
db,
module,
expander,
iter,
src.file_id,
container,
limit - 1,
));
expander.exit(db, mark);
}
}
}
}
items
}

120
crates/hir_def/src/db.rs Normal file
View file

@ -0,0 +1,120 @@
//! Defines database & queries for name resolution.
use std::sync::Arc;
use base_db::{salsa, CrateId, SourceDatabase, Upcast};
use hir_expand::{db::AstDatabase, HirFileId};
use syntax::SmolStr;
use crate::{
adt::{EnumData, StructData},
attr::Attrs,
body::{scope::ExprScopes, Body, BodySourceMap},
data::{ConstData, FunctionData, ImplData, StaticData, TraitData, TypeAliasData},
docs::Documentation,
generics::GenericParams,
import_map::ImportMap,
item_tree::ItemTree,
lang_item::{LangItemTarget, LangItems},
nameres::CrateDefMap,
AttrDefId, ConstId, ConstLoc, DefWithBodyId, EnumId, EnumLoc, FunctionId, FunctionLoc,
GenericDefId, ImplId, ImplLoc, ModuleId, StaticId, StaticLoc, StructId, StructLoc, TraitId,
TraitLoc, TypeAliasId, TypeAliasLoc, UnionId, UnionLoc,
};
#[salsa::query_group(InternDatabaseStorage)]
pub trait InternDatabase: SourceDatabase {
#[salsa::interned]
fn intern_function(&self, loc: FunctionLoc) -> FunctionId;
#[salsa::interned]
fn intern_struct(&self, loc: StructLoc) -> StructId;
#[salsa::interned]
fn intern_union(&self, loc: UnionLoc) -> UnionId;
#[salsa::interned]
fn intern_enum(&self, loc: EnumLoc) -> EnumId;
#[salsa::interned]
fn intern_const(&self, loc: ConstLoc) -> ConstId;
#[salsa::interned]
fn intern_static(&self, loc: StaticLoc) -> StaticId;
#[salsa::interned]
fn intern_trait(&self, loc: TraitLoc) -> TraitId;
#[salsa::interned]
fn intern_type_alias(&self, loc: TypeAliasLoc) -> TypeAliasId;
#[salsa::interned]
fn intern_impl(&self, loc: ImplLoc) -> ImplId;
}
#[salsa::query_group(DefDatabaseStorage)]
pub trait DefDatabase: InternDatabase + AstDatabase + Upcast<dyn AstDatabase> {
#[salsa::invoke(ItemTree::item_tree_query)]
fn item_tree(&self, file_id: HirFileId) -> Arc<ItemTree>;
#[salsa::invoke(crate_def_map_wait)]
#[salsa::transparent]
fn crate_def_map(&self, krate: CrateId) -> Arc<CrateDefMap>;
#[salsa::invoke(CrateDefMap::crate_def_map_query)]
fn crate_def_map_query(&self, krate: CrateId) -> Arc<CrateDefMap>;
#[salsa::invoke(StructData::struct_data_query)]
fn struct_data(&self, id: StructId) -> Arc<StructData>;
#[salsa::invoke(StructData::union_data_query)]
fn union_data(&self, id: UnionId) -> Arc<StructData>;
#[salsa::invoke(EnumData::enum_data_query)]
fn enum_data(&self, e: EnumId) -> Arc<EnumData>;
#[salsa::invoke(ImplData::impl_data_query)]
fn impl_data(&self, e: ImplId) -> Arc<ImplData>;
#[salsa::invoke(TraitData::trait_data_query)]
fn trait_data(&self, e: TraitId) -> Arc<TraitData>;
#[salsa::invoke(TypeAliasData::type_alias_data_query)]
fn type_alias_data(&self, e: TypeAliasId) -> Arc<TypeAliasData>;
#[salsa::invoke(FunctionData::fn_data_query)]
fn function_data(&self, func: FunctionId) -> Arc<FunctionData>;
#[salsa::invoke(ConstData::const_data_query)]
fn const_data(&self, konst: ConstId) -> Arc<ConstData>;
#[salsa::invoke(StaticData::static_data_query)]
fn static_data(&self, konst: StaticId) -> Arc<StaticData>;
#[salsa::invoke(Body::body_with_source_map_query)]
fn body_with_source_map(&self, def: DefWithBodyId) -> (Arc<Body>, Arc<BodySourceMap>);
#[salsa::invoke(Body::body_query)]
fn body(&self, def: DefWithBodyId) -> Arc<Body>;
#[salsa::invoke(ExprScopes::expr_scopes_query)]
fn expr_scopes(&self, def: DefWithBodyId) -> Arc<ExprScopes>;
#[salsa::invoke(GenericParams::generic_params_query)]
fn generic_params(&self, def: GenericDefId) -> Arc<GenericParams>;
#[salsa::invoke(Attrs::attrs_query)]
fn attrs(&self, def: AttrDefId) -> Attrs;
#[salsa::invoke(LangItems::module_lang_items_query)]
fn module_lang_items(&self, module: ModuleId) -> Option<Arc<LangItems>>;
#[salsa::invoke(LangItems::crate_lang_items_query)]
fn crate_lang_items(&self, krate: CrateId) -> Arc<LangItems>;
#[salsa::invoke(LangItems::lang_item_query)]
fn lang_item(&self, start_crate: CrateId, item: SmolStr) -> Option<LangItemTarget>;
// FIXME(https://github.com/rust-analyzer/rust-analyzer/issues/2148#issuecomment-550519102)
// Remove this query completely, in favor of `Attrs::docs` method
#[salsa::invoke(Documentation::documentation_query)]
fn documentation(&self, def: AttrDefId) -> Option<Documentation>;
#[salsa::invoke(ImportMap::import_map_query)]
fn import_map(&self, krate: CrateId) -> Arc<ImportMap>;
}
fn crate_def_map_wait(db: &impl DefDatabase, krate: CrateId) -> Arc<CrateDefMap> {
let _p = profile::span("crate_def_map:wait");
db.crate_def_map_query(krate)
}

View file

@ -0,0 +1,30 @@
//! Diagnostics produced by `hir_def`.
use std::any::Any;
use hir_expand::diagnostics::{Diagnostic, DiagnosticCode};
use syntax::{ast, AstPtr, SyntaxNodePtr};
use hir_expand::{HirFileId, InFile};
#[derive(Debug)]
pub struct UnresolvedModule {
pub file: HirFileId,
pub decl: AstPtr<ast::Module>,
pub candidate: String,
}
impl Diagnostic for UnresolvedModule {
fn code(&self) -> DiagnosticCode {
DiagnosticCode("unresolved-module")
}
fn message(&self) -> String {
"unresolved module".to_string()
}
fn display_source(&self) -> InFile<SyntaxNodePtr> {
InFile::new(self.file, self.decl.clone().into())
}
fn as_any(&self) -> &(dyn Any + Send + 'static) {
self
}
}

121
crates/hir_def/src/docs.rs Normal file
View file

@ -0,0 +1,121 @@
//! Defines hir documentation.
//!
//! This really shouldn't exist, instead, we should deshugar doc comments into attributes, see
//! https://github.com/rust-analyzer/rust-analyzer/issues/2148#issuecomment-550519102
use std::sync::Arc;
use either::Either;
use syntax::ast;
use crate::{
db::DefDatabase,
src::{HasChildSource, HasSource},
AdtId, AttrDefId, Lookup,
};
/// Holds documentation
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Documentation(Arc<str>);
impl Into<String> for Documentation {
fn into(self) -> String {
self.as_str().to_owned()
}
}
impl Documentation {
fn new(s: &str) -> Documentation {
Documentation(s.into())
}
pub fn from_ast<N>(node: &N) -> Option<Documentation>
where
N: ast::DocCommentsOwner + ast::AttrsOwner,
{
docs_from_ast(node)
}
pub fn as_str(&self) -> &str {
&*self.0
}
pub(crate) fn documentation_query(
db: &dyn DefDatabase,
def: AttrDefId,
) -> Option<Documentation> {
match def {
AttrDefId::ModuleId(module) => {
let def_map = db.crate_def_map(module.krate);
let src = def_map[module.local_id].declaration_source(db)?;
docs_from_ast(&src.value)
}
AttrDefId::FieldId(it) => {
let src = it.parent.child_source(db);
match &src.value[it.local_id] {
Either::Left(_tuple) => None,
Either::Right(record) => docs_from_ast(record),
}
}
AttrDefId::AdtId(it) => match it {
AdtId::StructId(it) => docs_from_ast(&it.lookup(db).source(db).value),
AdtId::EnumId(it) => docs_from_ast(&it.lookup(db).source(db).value),
AdtId::UnionId(it) => docs_from_ast(&it.lookup(db).source(db).value),
},
AttrDefId::EnumVariantId(it) => {
let src = it.parent.child_source(db);
docs_from_ast(&src.value[it.local_id])
}
AttrDefId::TraitId(it) => docs_from_ast(&it.lookup(db).source(db).value),
AttrDefId::MacroDefId(it) => docs_from_ast(&it.ast_id?.to_node(db.upcast())),
AttrDefId::ConstId(it) => docs_from_ast(&it.lookup(db).source(db).value),
AttrDefId::StaticId(it) => docs_from_ast(&it.lookup(db).source(db).value),
AttrDefId::FunctionId(it) => docs_from_ast(&it.lookup(db).source(db).value),
AttrDefId::TypeAliasId(it) => docs_from_ast(&it.lookup(db).source(db).value),
AttrDefId::ImplId(_) => None,
}
}
}
pub(crate) fn docs_from_ast<N>(node: &N) -> Option<Documentation>
where
N: ast::DocCommentsOwner + ast::AttrsOwner,
{
let doc_comment_text = node.doc_comment_text();
let doc_attr_text = expand_doc_attrs(node);
let docs = merge_doc_comments_and_attrs(doc_comment_text, doc_attr_text);
docs.map(|it| Documentation::new(&it))
}
fn merge_doc_comments_and_attrs(
doc_comment_text: Option<String>,
doc_attr_text: Option<String>,
) -> Option<String> {
match (doc_comment_text, doc_attr_text) {
(Some(mut comment_text), Some(attr_text)) => {
comment_text.push_str("\n\n");
comment_text.push_str(&attr_text);
Some(comment_text)
}
(Some(comment_text), None) => Some(comment_text),
(None, Some(attr_text)) => Some(attr_text),
(None, None) => None,
}
}
fn expand_doc_attrs(owner: &dyn ast::AttrsOwner) -> Option<String> {
let mut docs = String::new();
for attr in owner.attrs() {
if let Some(("doc", value)) =
attr.as_simple_key_value().as_ref().map(|(k, v)| (k.as_str(), v.as_str()))
{
docs.push_str(value);
docs.push_str("\n\n");
}
}
if docs.is_empty() {
None
} else {
Some(docs.trim_end_matches("\n\n").to_owned())
}
}

420
crates/hir_def/src/expr.rs Normal file
View file

@ -0,0 +1,420 @@
//! This module describes hir-level representation of expressions.
//!
//! This representaion is:
//!
//! 1. Identity-based. Each expression has an `id`, so we can distinguish
//! between different `1` in `1 + 1`.
//! 2. Independent of syntax. Though syntactic provenance information can be
//! attached separately via id-based side map.
//! 3. Unresolved. Paths are stored as sequences of names, and not as defs the
//! names refer to.
//! 4. Desugared. There's no `if let`.
//!
//! See also a neighboring `body` module.
use arena::{Idx, RawId};
use hir_expand::name::Name;
use syntax::ast::RangeOp;
use crate::{
builtin_type::{BuiltinFloat, BuiltinInt},
path::{GenericArgs, Path},
type_ref::{Mutability, Rawness, TypeRef},
};
pub type ExprId = Idx<Expr>;
pub(crate) fn dummy_expr_id() -> ExprId {
ExprId::from_raw(RawId::from(!0))
}
pub type PatId = Idx<Pat>;
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum Literal {
String(String),
ByteString(Vec<u8>),
Char(char),
Bool(bool),
Int(u64, Option<BuiltinInt>),
Float(u64, Option<BuiltinFloat>), // FIXME: f64 is not Eq
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum Expr {
/// This is produced if the syntax tree does not have a required expression piece.
Missing,
Path(Path),
If {
condition: ExprId,
then_branch: ExprId,
else_branch: Option<ExprId>,
},
Block {
statements: Vec<Statement>,
tail: Option<ExprId>,
label: Option<Name>,
},
Loop {
body: ExprId,
label: Option<Name>,
},
While {
condition: ExprId,
body: ExprId,
label: Option<Name>,
},
For {
iterable: ExprId,
pat: PatId,
body: ExprId,
label: Option<Name>,
},
Call {
callee: ExprId,
args: Vec<ExprId>,
},
MethodCall {
receiver: ExprId,
method_name: Name,
args: Vec<ExprId>,
generic_args: Option<GenericArgs>,
},
Match {
expr: ExprId,
arms: Vec<MatchArm>,
},
Continue {
label: Option<Name>,
},
Break {
expr: Option<ExprId>,
label: Option<Name>,
},
Return {
expr: Option<ExprId>,
},
RecordLit {
path: Option<Path>,
fields: Vec<RecordLitField>,
spread: Option<ExprId>,
},
Field {
expr: ExprId,
name: Name,
},
Await {
expr: ExprId,
},
Try {
expr: ExprId,
},
TryBlock {
body: ExprId,
},
Cast {
expr: ExprId,
type_ref: TypeRef,
},
Ref {
expr: ExprId,
rawness: Rawness,
mutability: Mutability,
},
Box {
expr: ExprId,
},
UnaryOp {
expr: ExprId,
op: UnaryOp,
},
BinaryOp {
lhs: ExprId,
rhs: ExprId,
op: Option<BinaryOp>,
},
Range {
lhs: Option<ExprId>,
rhs: Option<ExprId>,
range_type: RangeOp,
},
Index {
base: ExprId,
index: ExprId,
},
Lambda {
args: Vec<PatId>,
arg_types: Vec<Option<TypeRef>>,
ret_type: Option<TypeRef>,
body: ExprId,
},
Tuple {
exprs: Vec<ExprId>,
},
Unsafe {
body: ExprId,
},
Array(Array),
Literal(Literal),
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub enum BinaryOp {
LogicOp(LogicOp),
ArithOp(ArithOp),
CmpOp(CmpOp),
Assignment { op: Option<ArithOp> },
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub enum LogicOp {
And,
Or,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub enum CmpOp {
Eq { negated: bool },
Ord { ordering: Ordering, strict: bool },
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub enum Ordering {
Less,
Greater,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub enum ArithOp {
Add,
Mul,
Sub,
Div,
Rem,
Shl,
Shr,
BitXor,
BitOr,
BitAnd,
}
pub use syntax::ast::PrefixOp as UnaryOp;
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum Array {
ElementList(Vec<ExprId>),
Repeat { initializer: ExprId, repeat: ExprId },
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct MatchArm {
pub pat: PatId,
pub guard: Option<ExprId>,
pub expr: ExprId,
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct RecordLitField {
pub name: Name,
pub expr: ExprId,
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum Statement {
Let { pat: PatId, type_ref: Option<TypeRef>, initializer: Option<ExprId> },
Expr(ExprId),
}
impl Expr {
pub fn walk_child_exprs(&self, mut f: impl FnMut(ExprId)) {
match self {
Expr::Missing => {}
Expr::Path(_) => {}
Expr::If { condition, then_branch, else_branch } => {
f(*condition);
f(*then_branch);
if let Some(else_branch) = else_branch {
f(*else_branch);
}
}
Expr::Block { statements, tail, .. } => {
for stmt in statements {
match stmt {
Statement::Let { initializer, .. } => {
if let Some(expr) = initializer {
f(*expr);
}
}
Statement::Expr(e) => f(*e),
}
}
if let Some(expr) = tail {
f(*expr);
}
}
Expr::TryBlock { body } | Expr::Unsafe { body } => f(*body),
Expr::Loop { body, .. } => f(*body),
Expr::While { condition, body, .. } => {
f(*condition);
f(*body);
}
Expr::For { iterable, body, .. } => {
f(*iterable);
f(*body);
}
Expr::Call { callee, args } => {
f(*callee);
for arg in args {
f(*arg);
}
}
Expr::MethodCall { receiver, args, .. } => {
f(*receiver);
for arg in args {
f(*arg);
}
}
Expr::Match { expr, arms } => {
f(*expr);
for arm in arms {
f(arm.expr);
}
}
Expr::Continue { .. } => {}
Expr::Break { expr, .. } | Expr::Return { expr } => {
if let Some(expr) = expr {
f(*expr);
}
}
Expr::RecordLit { fields, spread, .. } => {
for field in fields {
f(field.expr);
}
if let Some(expr) = spread {
f(*expr);
}
}
Expr::Lambda { body, .. } => {
f(*body);
}
Expr::BinaryOp { lhs, rhs, .. } => {
f(*lhs);
f(*rhs);
}
Expr::Range { lhs, rhs, .. } => {
if let Some(lhs) = rhs {
f(*lhs);
}
if let Some(rhs) = lhs {
f(*rhs);
}
}
Expr::Index { base, index } => {
f(*base);
f(*index);
}
Expr::Field { expr, .. }
| Expr::Await { expr }
| Expr::Try { expr }
| Expr::Cast { expr, .. }
| Expr::Ref { expr, .. }
| Expr::UnaryOp { expr, .. }
| Expr::Box { expr } => {
f(*expr);
}
Expr::Tuple { exprs } => {
for expr in exprs {
f(*expr);
}
}
Expr::Array(a) => match a {
Array::ElementList(exprs) => {
for expr in exprs {
f(*expr);
}
}
Array::Repeat { initializer, repeat } => {
f(*initializer);
f(*repeat)
}
},
Expr::Literal(_) => {}
}
}
}
/// Explicit binding annotations given in the HIR for a binding. Note
/// that this is not the final binding *mode* that we infer after type
/// inference.
#[derive(Clone, PartialEq, Eq, Debug, Copy)]
pub enum BindingAnnotation {
/// No binding annotation given: this means that the final binding mode
/// will depend on whether we have skipped through a `&` reference
/// when matching. For example, the `x` in `Some(x)` will have binding
/// mode `None`; if you do `let Some(x) = &Some(22)`, it will
/// ultimately be inferred to be by-reference.
Unannotated,
/// Annotated with `mut x` -- could be either ref or not, similar to `None`.
Mutable,
/// Annotated as `ref`, like `ref x`
Ref,
/// Annotated as `ref mut x`.
RefMut,
}
impl BindingAnnotation {
pub fn new(is_mutable: bool, is_ref: bool) -> Self {
match (is_mutable, is_ref) {
(true, true) => BindingAnnotation::RefMut,
(false, true) => BindingAnnotation::Ref,
(true, false) => BindingAnnotation::Mutable,
(false, false) => BindingAnnotation::Unannotated,
}
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct RecordFieldPat {
pub name: Name,
pub pat: PatId,
}
/// Close relative to rustc's hir::PatKind
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum Pat {
Missing,
Wild,
Tuple { args: Vec<PatId>, ellipsis: Option<usize> },
Or(Vec<PatId>),
Record { path: Option<Path>, args: Vec<RecordFieldPat>, ellipsis: bool },
Range { start: ExprId, end: ExprId },
Slice { prefix: Vec<PatId>, slice: Option<PatId>, suffix: Vec<PatId> },
Path(Path),
Lit(ExprId),
Bind { mode: BindingAnnotation, name: Name, subpat: Option<PatId> },
TupleStruct { path: Option<Path>, args: Vec<PatId>, ellipsis: Option<usize> },
Ref { pat: PatId, mutability: Mutability },
}
impl Pat {
pub fn walk_child_pats(&self, mut f: impl FnMut(PatId)) {
match self {
Pat::Range { .. } | Pat::Lit(..) | Pat::Path(..) | Pat::Wild | Pat::Missing => {}
Pat::Bind { subpat, .. } => {
subpat.iter().copied().for_each(f);
}
Pat::Or(args) | Pat::Tuple { args, .. } | Pat::TupleStruct { args, .. } => {
args.iter().copied().for_each(f);
}
Pat::Ref { pat, .. } => f(*pat),
Pat::Slice { prefix, slice, suffix } => {
let total_iter = prefix.iter().chain(slice.iter()).chain(suffix.iter());
total_iter.copied().for_each(f);
}
Pat::Record { args, .. } => {
args.iter().map(|f| f.pat).for_each(f);
}
}
}
}

View file

@ -0,0 +1,687 @@
//! An algorithm to find a path to refer to a certain item.
use hir_expand::name::{known, AsName, Name};
use rustc_hash::FxHashSet;
use test_utils::mark;
use crate::{
db::DefDatabase,
item_scope::ItemInNs,
path::{ModPath, PathKind},
visibility::Visibility,
ModuleDefId, ModuleId,
};
// FIXME: handle local items
/// Find a path that can be used to refer to a certain item. This can depend on
/// *from where* you're referring to the item, hence the `from` parameter.
pub fn find_path(db: &dyn DefDatabase, item: ItemInNs, from: ModuleId) -> Option<ModPath> {
let _p = profile::span("find_path");
find_path_inner(db, item, from, MAX_PATH_LEN)
}
const MAX_PATH_LEN: usize = 15;
impl ModPath {
fn starts_with_std(&self) -> bool {
self.segments.first() == Some(&known::std)
}
// When std library is present, paths starting with `std::`
// should be preferred over paths starting with `core::` and `alloc::`
fn can_start_with_std(&self) -> bool {
let first_segment = self.segments.first();
first_segment == Some(&known::alloc) || first_segment == Some(&known::core)
}
}
fn find_path_inner(
db: &dyn DefDatabase,
item: ItemInNs,
from: ModuleId,
max_len: usize,
) -> Option<ModPath> {
if max_len == 0 {
return None;
}
// Base cases:
// - if the item is already in scope, return the name under which it is
let def_map = db.crate_def_map(from.krate);
let from_scope: &crate::item_scope::ItemScope = &def_map.modules[from.local_id].scope;
if let Some((name, _)) = from_scope.name_of(item) {
return Some(ModPath::from_segments(PathKind::Plain, vec![name.clone()]));
}
// - if the item is the crate root, return `crate`
if item
== ItemInNs::Types(ModuleDefId::ModuleId(ModuleId {
krate: from.krate,
local_id: def_map.root,
}))
{
return Some(ModPath::from_segments(PathKind::Crate, Vec::new()));
}
// - if the item is the module we're in, use `self`
if item == ItemInNs::Types(from.into()) {
return Some(ModPath::from_segments(PathKind::Super(0), Vec::new()));
}
// - if the item is the parent module, use `super` (this is not used recursively, since `super::super` is ugly)
if let Some(parent_id) = def_map.modules[from.local_id].parent {
if item
== ItemInNs::Types(ModuleDefId::ModuleId(ModuleId {
krate: from.krate,
local_id: parent_id,
}))
{
return Some(ModPath::from_segments(PathKind::Super(1), Vec::new()));
}
}
// - if the item is the crate root of a dependency crate, return the name from the extern prelude
for (name, def_id) in &def_map.extern_prelude {
if item == ItemInNs::Types(*def_id) {
return Some(ModPath::from_segments(PathKind::Plain, vec![name.clone()]));
}
}
// - if the item is in the prelude, return the name from there
if let Some(prelude_module) = def_map.prelude {
let prelude_def_map = db.crate_def_map(prelude_module.krate);
let prelude_scope: &crate::item_scope::ItemScope =
&prelude_def_map.modules[prelude_module.local_id].scope;
if let Some((name, vis)) = prelude_scope.name_of(item) {
if vis.is_visible_from(db, from) {
return Some(ModPath::from_segments(PathKind::Plain, vec![name.clone()]));
}
}
}
// - if the item is a builtin, it's in scope
if let ItemInNs::Types(ModuleDefId::BuiltinType(builtin)) = item {
return Some(ModPath::from_segments(PathKind::Plain, vec![builtin.as_name()]));
}
// Recursive case:
// - if the item is an enum variant, refer to it via the enum
if let Some(ModuleDefId::EnumVariantId(variant)) = item.as_module_def_id() {
if let Some(mut path) = find_path(db, ItemInNs::Types(variant.parent.into()), from) {
let data = db.enum_data(variant.parent);
path.segments.push(data.variants[variant.local_id].name.clone());
return Some(path);
}
// If this doesn't work, it seems we have no way of referring to the
// enum; that's very weird, but there might still be a reexport of the
// variant somewhere
}
// - otherwise, look for modules containing (reexporting) it and import it from one of those
let crate_root = ModuleId { local_id: def_map.root, krate: from.krate };
let crate_attrs = db.attrs(crate_root.into());
let prefer_no_std = crate_attrs.by_key("no_std").exists();
let mut best_path = None;
let mut best_path_len = max_len;
if item.krate(db) == Some(from.krate) {
// Item was defined in the same crate that wants to import it. It cannot be found in any
// dependency in this case.
let local_imports = find_local_import_locations(db, item, from);
for (module_id, name) in local_imports {
if let Some(mut path) = find_path_inner(
db,
ItemInNs::Types(ModuleDefId::ModuleId(module_id)),
from,
best_path_len - 1,
) {
path.segments.push(name);
let new_path = if let Some(best_path) = best_path {
select_best_path(best_path, path, prefer_no_std)
} else {
path
};
best_path_len = new_path.len();
best_path = Some(new_path);
}
}
} else {
// Item was defined in some upstream crate. This means that it must be exported from one,
// too (unless we can't name it at all). It could *also* be (re)exported by the same crate
// that wants to import it here, but we always prefer to use the external path here.
let crate_graph = db.crate_graph();
let extern_paths = crate_graph[from.krate].dependencies.iter().filter_map(|dep| {
let import_map = db.import_map(dep.crate_id);
import_map.import_info_for(item).and_then(|info| {
// Determine best path for containing module and append last segment from `info`.
let mut path = find_path_inner(
db,
ItemInNs::Types(ModuleDefId::ModuleId(info.container)),
from,
best_path_len - 1,
)?;
path.segments.push(info.path.segments.last().unwrap().clone());
Some(path)
})
});
for path in extern_paths {
let new_path = if let Some(best_path) = best_path {
select_best_path(best_path, path, prefer_no_std)
} else {
path
};
best_path = Some(new_path);
}
}
best_path
}
fn select_best_path(old_path: ModPath, new_path: ModPath, prefer_no_std: bool) -> ModPath {
if old_path.starts_with_std() && new_path.can_start_with_std() {
if prefer_no_std {
mark::hit!(prefer_no_std_paths);
new_path
} else {
mark::hit!(prefer_std_paths);
old_path
}
} else if new_path.starts_with_std() && old_path.can_start_with_std() {
if prefer_no_std {
mark::hit!(prefer_no_std_paths);
old_path
} else {
mark::hit!(prefer_std_paths);
new_path
}
} else if new_path.len() < old_path.len() {
new_path
} else {
old_path
}
}
/// Finds locations in `from.krate` from which `item` can be imported by `from`.
fn find_local_import_locations(
db: &dyn DefDatabase,
item: ItemInNs,
from: ModuleId,
) -> Vec<(ModuleId, Name)> {
let _p = profile::span("find_local_import_locations");
// `from` can import anything below `from` with visibility of at least `from`, and anything
// above `from` with any visibility. That means we do not need to descend into private siblings
// of `from` (and similar).
let def_map = db.crate_def_map(from.krate);
// Compute the initial worklist. We start with all direct child modules of `from` as well as all
// of its (recursive) parent modules.
let data = &def_map.modules[from.local_id];
let mut worklist = data
.children
.values()
.map(|child| ModuleId { krate: from.krate, local_id: *child })
.collect::<Vec<_>>();
let mut parent = data.parent;
while let Some(p) = parent {
worklist.push(ModuleId { krate: from.krate, local_id: p });
parent = def_map.modules[p].parent;
}
let mut seen: FxHashSet<_> = FxHashSet::default();
let mut locations = Vec::new();
while let Some(module) = worklist.pop() {
if !seen.insert(module) {
continue; // already processed this module
}
let ext_def_map;
let data = if module.krate == from.krate {
&def_map[module.local_id]
} else {
// The crate might reexport a module defined in another crate.
ext_def_map = db.crate_def_map(module.krate);
&ext_def_map[module.local_id]
};
if let Some((name, vis)) = data.scope.name_of(item) {
if vis.is_visible_from(db, from) {
let is_private = if let Visibility::Module(private_to) = vis {
private_to.local_id == module.local_id
} else {
false
};
let is_original_def = if let Some(module_def_id) = item.as_module_def_id() {
data.scope.declarations().any(|it| it == module_def_id)
} else {
false
};
// Ignore private imports. these could be used if we are
// in a submodule of this module, but that's usually not
// what the user wants; and if this module can import
// the item and we're a submodule of it, so can we.
// Also this keeps the cached data smaller.
if !is_private || is_original_def {
locations.push((module, name.clone()));
}
}
}
// Descend into all modules visible from `from`.
for (_, per_ns) in data.scope.entries() {
if let Some((ModuleDefId::ModuleId(module), vis)) = per_ns.take_types_vis() {
if vis.is_visible_from(db, from) {
worklist.push(module);
}
}
}
}
locations
}
#[cfg(test)]
mod tests {
use base_db::fixture::WithFixture;
use hir_expand::hygiene::Hygiene;
use syntax::ast::AstNode;
use test_utils::mark;
use crate::test_db::TestDB;
use super::*;
/// `code` needs to contain a cursor marker; checks that `find_path` for the
/// item the `path` refers to returns that same path when called from the
/// module the cursor is in.
fn check_found_path(ra_fixture: &str, path: &str) {
let (db, pos) = TestDB::with_position(ra_fixture);
let module = db.module_for_file(pos.file_id);
let parsed_path_file = syntax::SourceFile::parse(&format!("use {};", path));
let ast_path =
parsed_path_file.syntax_node().descendants().find_map(syntax::ast::Path::cast).unwrap();
let mod_path = ModPath::from_src(ast_path, &Hygiene::new_unhygienic()).unwrap();
let crate_def_map = db.crate_def_map(module.krate);
let resolved = crate_def_map
.resolve_path(
&db,
module.local_id,
&mod_path,
crate::item_scope::BuiltinShadowMode::Module,
)
.0
.take_types()
.unwrap();
let found_path = find_path(&db, ItemInNs::Types(resolved), module);
assert_eq!(found_path, Some(mod_path));
}
#[test]
fn same_module() {
let code = r#"
//- /main.rs
struct S;
<|>
"#;
check_found_path(code, "S");
}
#[test]
fn enum_variant() {
let code = r#"
//- /main.rs
enum E { A }
<|>
"#;
check_found_path(code, "E::A");
}
#[test]
fn sub_module() {
let code = r#"
//- /main.rs
mod foo {
pub struct S;
}
<|>
"#;
check_found_path(code, "foo::S");
}
#[test]
fn super_module() {
let code = r#"
//- /main.rs
mod foo;
//- /foo.rs
mod bar;
struct S;
//- /foo/bar.rs
<|>
"#;
check_found_path(code, "super::S");
}
#[test]
fn self_module() {
let code = r#"
//- /main.rs
mod foo;
//- /foo.rs
<|>
"#;
check_found_path(code, "self");
}
#[test]
fn crate_root() {
let code = r#"
//- /main.rs
mod foo;
//- /foo.rs
<|>
"#;
check_found_path(code, "crate");
}
#[test]
fn same_crate() {
let code = r#"
//- /main.rs
mod foo;
struct S;
//- /foo.rs
<|>
"#;
check_found_path(code, "crate::S");
}
#[test]
fn different_crate() {
let code = r#"
//- /main.rs crate:main deps:std
<|>
//- /std.rs crate:std
pub struct S;
"#;
check_found_path(code, "std::S");
}
#[test]
fn different_crate_renamed() {
let code = r#"
//- /main.rs crate:main deps:std
extern crate std as std_renamed;
<|>
//- /std.rs crate:std
pub struct S;
"#;
check_found_path(code, "std_renamed::S");
}
#[test]
fn partially_imported() {
// Tests that short paths are used even for external items, when parts of the path are
// already in scope.
check_found_path(
r#"
//- /main.rs crate:main deps:syntax
use syntax::ast;
<|>
//- /lib.rs crate:syntax
pub mod ast {
pub enum ModuleItem {
A, B, C,
}
}
"#,
"ast::ModuleItem",
);
check_found_path(
r#"
//- /main.rs crate:main deps:syntax
<|>
//- /lib.rs crate:syntax
pub mod ast {
pub enum ModuleItem {
A, B, C,
}
}
"#,
"syntax::ast::ModuleItem",
);
}
#[test]
fn same_crate_reexport() {
let code = r#"
//- /main.rs
mod bar {
mod foo { pub(super) struct S; }
pub(crate) use foo::*;
}
<|>
"#;
check_found_path(code, "bar::S");
}
#[test]
fn same_crate_reexport_rename() {
let code = r#"
//- /main.rs
mod bar {
mod foo { pub(super) struct S; }
pub(crate) use foo::S as U;
}
<|>
"#;
check_found_path(code, "bar::U");
}
#[test]
fn different_crate_reexport() {
let code = r#"
//- /main.rs crate:main deps:std
<|>
//- /std.rs crate:std deps:core
pub use core::S;
//- /core.rs crate:core
pub struct S;
"#;
check_found_path(code, "std::S");
}
#[test]
fn prelude() {
let code = r#"
//- /main.rs crate:main deps:std
<|>
//- /std.rs crate:std
pub mod prelude { pub struct S; }
#[prelude_import]
pub use prelude::*;
"#;
check_found_path(code, "S");
}
#[test]
fn enum_variant_from_prelude() {
let code = r#"
//- /main.rs crate:main deps:std
<|>
//- /std.rs crate:std
pub mod prelude {
pub enum Option<T> { Some(T), None }
pub use Option::*;
}
#[prelude_import]
pub use prelude::*;
"#;
check_found_path(code, "None");
check_found_path(code, "Some");
}
#[test]
fn shortest_path() {
let code = r#"
//- /main.rs
pub mod foo;
pub mod baz;
struct S;
<|>
//- /foo.rs
pub mod bar { pub struct S; }
//- /baz.rs
pub use crate::foo::bar::S;
"#;
check_found_path(code, "baz::S");
}
#[test]
fn discount_private_imports() {
let code = r#"
//- /main.rs
mod foo;
pub mod bar { pub struct S; }
use bar::S;
//- /foo.rs
<|>
"#;
// crate::S would be shorter, but using private imports seems wrong
check_found_path(code, "crate::bar::S");
}
#[test]
fn import_cycle() {
let code = r#"
//- /main.rs
pub mod foo;
pub mod bar;
pub mod baz;
//- /bar.rs
<|>
//- /foo.rs
pub use super::baz;
pub struct S;
//- /baz.rs
pub use super::foo;
"#;
check_found_path(code, "crate::foo::S");
}
#[test]
fn prefer_std_paths_over_alloc() {
mark::check!(prefer_std_paths);
let code = r#"
//- /main.rs crate:main deps:alloc,std
<|>
//- /std.rs crate:std deps:alloc
pub mod sync {
pub use alloc::sync::Arc;
}
//- /zzz.rs crate:alloc
pub mod sync {
pub struct Arc;
}
"#;
check_found_path(code, "std::sync::Arc");
}
#[test]
fn prefer_core_paths_over_std() {
mark::check!(prefer_no_std_paths);
let code = r#"
//- /main.rs crate:main deps:core,std
#![no_std]
<|>
//- /std.rs crate:std deps:core
pub mod fmt {
pub use core::fmt::Error;
}
//- /zzz.rs crate:core
pub mod fmt {
pub struct Error;
}
"#;
check_found_path(code, "core::fmt::Error");
}
#[test]
fn prefer_alloc_paths_over_std() {
let code = r#"
//- /main.rs crate:main deps:alloc,std
#![no_std]
<|>
//- /std.rs crate:std deps:alloc
pub mod sync {
pub use alloc::sync::Arc;
}
//- /zzz.rs crate:alloc
pub mod sync {
pub struct Arc;
}
"#;
check_found_path(code, "alloc::sync::Arc");
}
#[test]
fn prefer_shorter_paths_if_not_alloc() {
let code = r#"
//- /main.rs crate:main deps:megaalloc,std
<|>
//- /std.rs crate:std deps:megaalloc
pub mod sync {
pub use megaalloc::sync::Arc;
}
//- /zzz.rs crate:megaalloc
pub struct Arc;
"#;
check_found_path(code, "megaalloc::Arc");
}
#[test]
fn builtins_are_in_scope() {
let code = r#"
//- /main.rs
<|>
pub mod primitive {
pub use u8;
}
"#;
check_found_path(code, "u8");
check_found_path(code, "u16");
}
}

View file

@ -0,0 +1,339 @@
//! Many kinds of items or constructs can have generic parameters: functions,
//! structs, impls, traits, etc. This module provides a common HIR for these
//! generic parameters. See also the `Generics` type and the `generics_of` query
//! in rustc.
use std::sync::Arc;
use arena::{map::ArenaMap, Arena};
use base_db::FileId;
use either::Either;
use hir_expand::{
name::{name, AsName, Name},
InFile,
};
use syntax::ast::{self, GenericParamsOwner, NameOwner, TypeBoundsOwner};
use crate::{
body::LowerCtx,
child_by_source::ChildBySource,
db::DefDatabase,
dyn_map::DynMap,
keys,
src::HasChildSource,
src::HasSource,
type_ref::{TypeBound, TypeRef},
AdtId, GenericDefId, LocalTypeParamId, Lookup, TypeParamId,
};
/// Data about a generic parameter (to a function, struct, impl, ...).
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct TypeParamData {
pub name: Option<Name>,
pub default: Option<TypeRef>,
pub provenance: TypeParamProvenance,
}
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum TypeParamProvenance {
TypeParamList,
TraitSelf,
ArgumentImplTrait,
}
/// Data about the generic parameters of a function, struct, impl, etc.
#[derive(Clone, PartialEq, Eq, Debug, Default)]
pub struct GenericParams {
pub types: Arena<TypeParamData>,
// lifetimes: Arena<LocalLifetimeParamId, LifetimeParamData>,
pub where_predicates: Vec<WherePredicate>,
}
/// A single predicate from a where clause, i.e. `where Type: Trait`. Combined
/// where clauses like `where T: Foo + Bar` are turned into multiple of these.
/// It might still result in multiple actual predicates though, because of
/// associated type bindings like `Iterator<Item = u32>`.
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct WherePredicate {
pub target: WherePredicateTarget,
pub bound: TypeBound,
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum WherePredicateTarget {
TypeRef(TypeRef),
/// For desugared where predicates that can directly refer to a type param.
TypeParam(LocalTypeParamId),
}
type SourceMap = ArenaMap<LocalTypeParamId, Either<ast::Trait, ast::TypeParam>>;
impl GenericParams {
pub(crate) fn generic_params_query(
db: &dyn DefDatabase,
def: GenericDefId,
) -> Arc<GenericParams> {
let _p = profile::span("generic_params_query");
let generics = match def {
GenericDefId::FunctionId(id) => {
let id = id.lookup(db).id;
let tree = db.item_tree(id.file_id);
let item = &tree[id.value];
tree[item.generic_params].clone()
}
GenericDefId::AdtId(AdtId::StructId(id)) => {
let id = id.lookup(db).id;
let tree = db.item_tree(id.file_id);
let item = &tree[id.value];
tree[item.generic_params].clone()
}
GenericDefId::AdtId(AdtId::EnumId(id)) => {
let id = id.lookup(db).id;
let tree = db.item_tree(id.file_id);
let item = &tree[id.value];
tree[item.generic_params].clone()
}
GenericDefId::AdtId(AdtId::UnionId(id)) => {
let id = id.lookup(db).id;
let tree = db.item_tree(id.file_id);
let item = &tree[id.value];
tree[item.generic_params].clone()
}
GenericDefId::TraitId(id) => {
let id = id.lookup(db).id;
let tree = db.item_tree(id.file_id);
let item = &tree[id.value];
tree[item.generic_params].clone()
}
GenericDefId::TypeAliasId(id) => {
let id = id.lookup(db).id;
let tree = db.item_tree(id.file_id);
let item = &tree[id.value];
tree[item.generic_params].clone()
}
GenericDefId::ImplId(id) => {
let id = id.lookup(db).id;
let tree = db.item_tree(id.file_id);
let item = &tree[id.value];
tree[item.generic_params].clone()
}
GenericDefId::EnumVariantId(_) | GenericDefId::ConstId(_) => GenericParams::default(),
};
Arc::new(generics)
}
fn new(db: &dyn DefDatabase, def: GenericDefId) -> (GenericParams, InFile<SourceMap>) {
let mut generics = GenericParams { types: Arena::default(), where_predicates: Vec::new() };
let mut sm = ArenaMap::default();
// FIXME: add `: Sized` bound for everything except for `Self` in traits
let file_id = match def {
GenericDefId::FunctionId(it) => {
let src = it.lookup(db).source(db);
let lower_ctx = LowerCtx::new(db, src.file_id);
generics.fill(&lower_ctx, &mut sm, &src.value);
// lower `impl Trait` in arguments
let data = db.function_data(it);
for param in &data.params {
generics.fill_implicit_impl_trait_args(param);
}
src.file_id
}
GenericDefId::AdtId(AdtId::StructId(it)) => {
let src = it.lookup(db).source(db);
let lower_ctx = LowerCtx::new(db, src.file_id);
generics.fill(&lower_ctx, &mut sm, &src.value);
src.file_id
}
GenericDefId::AdtId(AdtId::UnionId(it)) => {
let src = it.lookup(db).source(db);
let lower_ctx = LowerCtx::new(db, src.file_id);
generics.fill(&lower_ctx, &mut sm, &src.value);
src.file_id
}
GenericDefId::AdtId(AdtId::EnumId(it)) => {
let src = it.lookup(db).source(db);
let lower_ctx = LowerCtx::new(db, src.file_id);
generics.fill(&lower_ctx, &mut sm, &src.value);
src.file_id
}
GenericDefId::TraitId(it) => {
let src = it.lookup(db).source(db);
let lower_ctx = LowerCtx::new(db, src.file_id);
// traits get the Self type as an implicit first type parameter
let self_param_id = generics.types.alloc(TypeParamData {
name: Some(name![Self]),
default: None,
provenance: TypeParamProvenance::TraitSelf,
});
sm.insert(self_param_id, Either::Left(src.value.clone()));
// add super traits as bounds on Self
// i.e., trait Foo: Bar is equivalent to trait Foo where Self: Bar
let self_param = TypeRef::Path(name![Self].into());
generics.fill_bounds(&lower_ctx, &src.value, self_param);
generics.fill(&lower_ctx, &mut sm, &src.value);
src.file_id
}
GenericDefId::TypeAliasId(it) => {
let src = it.lookup(db).source(db);
let lower_ctx = LowerCtx::new(db, src.file_id);
generics.fill(&lower_ctx, &mut sm, &src.value);
src.file_id
}
// Note that we don't add `Self` here: in `impl`s, `Self` is not a
// type-parameter, but rather is a type-alias for impl's target
// type, so this is handled by the resolver.
GenericDefId::ImplId(it) => {
let src = it.lookup(db).source(db);
let lower_ctx = LowerCtx::new(db, src.file_id);
generics.fill(&lower_ctx, &mut sm, &src.value);
src.file_id
}
// We won't be using this ID anyway
GenericDefId::EnumVariantId(_) | GenericDefId::ConstId(_) => FileId(!0).into(),
};
(generics, InFile::new(file_id, sm))
}
pub(crate) fn fill(
&mut self,
lower_ctx: &LowerCtx,
sm: &mut SourceMap,
node: &dyn GenericParamsOwner,
) {
if let Some(params) = node.generic_param_list() {
self.fill_params(lower_ctx, sm, params)
}
if let Some(where_clause) = node.where_clause() {
self.fill_where_predicates(lower_ctx, where_clause);
}
}
pub(crate) fn fill_bounds(
&mut self,
lower_ctx: &LowerCtx,
node: &dyn ast::TypeBoundsOwner,
type_ref: TypeRef,
) {
for bound in
node.type_bound_list().iter().flat_map(|type_bound_list| type_bound_list.bounds())
{
self.add_where_predicate_from_bound(lower_ctx, bound, type_ref.clone());
}
}
fn fill_params(
&mut self,
lower_ctx: &LowerCtx,
sm: &mut SourceMap,
params: ast::GenericParamList,
) {
for type_param in params.type_params() {
let name = type_param.name().map_or_else(Name::missing, |it| it.as_name());
// FIXME: Use `Path::from_src`
let default = type_param.default_type().map(|it| TypeRef::from_ast(lower_ctx, it));
let param = TypeParamData {
name: Some(name.clone()),
default,
provenance: TypeParamProvenance::TypeParamList,
};
let param_id = self.types.alloc(param);
sm.insert(param_id, Either::Right(type_param.clone()));
let type_ref = TypeRef::Path(name.into());
self.fill_bounds(&lower_ctx, &type_param, type_ref);
}
}
fn fill_where_predicates(&mut self, lower_ctx: &LowerCtx, where_clause: ast::WhereClause) {
for pred in where_clause.predicates() {
let type_ref = match pred.ty() {
Some(type_ref) => type_ref,
None => continue,
};
let type_ref = TypeRef::from_ast(lower_ctx, type_ref);
for bound in pred.type_bound_list().iter().flat_map(|l| l.bounds()) {
self.add_where_predicate_from_bound(lower_ctx, bound, type_ref.clone());
}
}
}
fn add_where_predicate_from_bound(
&mut self,
lower_ctx: &LowerCtx,
bound: ast::TypeBound,
type_ref: TypeRef,
) {
if bound.question_mark_token().is_some() {
// FIXME: remove this bound
return;
}
let bound = TypeBound::from_ast(lower_ctx, bound);
self.where_predicates
.push(WherePredicate { target: WherePredicateTarget::TypeRef(type_ref), bound });
}
pub(crate) fn fill_implicit_impl_trait_args(&mut self, type_ref: &TypeRef) {
type_ref.walk(&mut |type_ref| {
if let TypeRef::ImplTrait(bounds) = type_ref {
let param = TypeParamData {
name: None,
default: None,
provenance: TypeParamProvenance::ArgumentImplTrait,
};
let param_id = self.types.alloc(param);
for bound in bounds {
self.where_predicates.push(WherePredicate {
target: WherePredicateTarget::TypeParam(param_id),
bound: bound.clone(),
});
}
}
});
}
pub fn find_by_name(&self, name: &Name) -> Option<LocalTypeParamId> {
self.types
.iter()
.find_map(|(id, p)| if p.name.as_ref() == Some(name) { Some(id) } else { None })
}
pub fn find_trait_self_param(&self) -> Option<LocalTypeParamId> {
self.types.iter().find_map(|(id, p)| {
if p.provenance == TypeParamProvenance::TraitSelf {
Some(id)
} else {
None
}
})
}
}
impl HasChildSource for GenericDefId {
type ChildId = LocalTypeParamId;
type Value = Either<ast::Trait, ast::TypeParam>;
fn child_source(&self, db: &dyn DefDatabase) -> InFile<SourceMap> {
let (_, sm) = GenericParams::new(db, *self);
sm
}
}
impl ChildBySource for GenericDefId {
fn child_by_source(&self, db: &dyn DefDatabase) -> DynMap {
let mut res = DynMap::default();
let arena_map = self.child_source(db);
let arena_map = arena_map.as_ref();
for (local_id, src) in arena_map.value.iter() {
let id = TypeParamId { parent: *self, local_id };
if let Either::Right(type_param) = src {
res[keys::TYPE_PARAM].insert(arena_map.with_value(type_param.clone()), id)
}
}
res
}
}

View file

@ -0,0 +1,745 @@
//! A map of all publicly exported items in a crate.
use std::{cmp::Ordering, fmt, hash::BuildHasherDefault, sync::Arc};
use base_db::CrateId;
use fst::{self, Streamer};
use indexmap::{map::Entry, IndexMap};
use rustc_hash::{FxHashMap, FxHasher};
use smallvec::SmallVec;
use syntax::SmolStr;
use crate::{
db::DefDatabase,
item_scope::ItemInNs,
path::{ModPath, PathKind},
visibility::Visibility,
AssocItemId, ModuleDefId, ModuleId, TraitId,
};
type FxIndexMap<K, V> = IndexMap<K, V, BuildHasherDefault<FxHasher>>;
/// Item import details stored in the `ImportMap`.
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct ImportInfo {
/// A path that can be used to import the item, relative to the crate's root.
pub path: ModPath,
/// The module containing this item.
pub container: ModuleId,
}
/// A map from publicly exported items to the path needed to import/name them from a downstream
/// crate.
///
/// Reexports of items are taken into account, ie. if something is exported under multiple
/// names, the one with the shortest import path will be used.
///
/// Note that all paths are relative to the containing crate's root, so the crate name still needs
/// to be prepended to the `ModPath` before the path is valid.
#[derive(Default)]
pub struct ImportMap {
map: FxIndexMap<ItemInNs, ImportInfo>,
/// List of keys stored in `map`, sorted lexicographically by their `ModPath`. Indexed by the
/// values returned by running `fst`.
///
/// Since a path can refer to multiple items due to namespacing, we store all items with the
/// same path right after each other. This allows us to find all items after the FST gives us
/// the index of the first one.
importables: Vec<ItemInNs>,
fst: fst::Map<Vec<u8>>,
/// Maps names of associated items to the item's ID. Only includes items whose defining trait is
/// exported.
assoc_map: FxHashMap<SmolStr, SmallVec<[AssocItemId; 1]>>,
}
impl ImportMap {
pub fn import_map_query(db: &dyn DefDatabase, krate: CrateId) -> Arc<Self> {
let _p = profile::span("import_map_query");
let def_map = db.crate_def_map(krate);
let mut import_map = Self::default();
// We look only into modules that are public(ly reexported), starting with the crate root.
let empty = ModPath { kind: PathKind::Plain, segments: vec![] };
let root = ModuleId { krate, local_id: def_map.root };
let mut worklist = vec![(root, empty)];
while let Some((module, mod_path)) = worklist.pop() {
let ext_def_map;
let mod_data = if module.krate == krate {
&def_map[module.local_id]
} else {
// The crate might reexport a module defined in another crate.
ext_def_map = db.crate_def_map(module.krate);
&ext_def_map[module.local_id]
};
let visible_items = mod_data.scope.entries().filter_map(|(name, per_ns)| {
let per_ns = per_ns.filter_visibility(|vis| vis == Visibility::Public);
if per_ns.is_none() {
None
} else {
Some((name, per_ns))
}
});
for (name, per_ns) in visible_items {
let mk_path = || {
let mut path = mod_path.clone();
path.segments.push(name.clone());
path
};
for item in per_ns.iter_items() {
let path = mk_path();
match import_map.map.entry(item) {
Entry::Vacant(entry) => {
entry.insert(ImportInfo { path, container: module });
}
Entry::Occupied(mut entry) => {
// If the new path is shorter, prefer that one.
if path.len() < entry.get().path.len() {
*entry.get_mut() = ImportInfo { path, container: module };
} else {
continue;
}
}
}
// If we've just added a path to a module, descend into it. We might traverse
// modules multiple times, but only if the new path to it is shorter than the
// first (else we `continue` above).
if let Some(ModuleDefId::ModuleId(mod_id)) = item.as_module_def_id() {
worklist.push((mod_id, mk_path()));
}
// If we've added a path to a trait, add the trait's methods to the method map.
if let Some(ModuleDefId::TraitId(tr)) = item.as_module_def_id() {
import_map.collect_trait_methods(db, tr);
}
}
}
}
let mut importables = import_map.map.iter().collect::<Vec<_>>();
importables.sort_by(cmp);
// Build the FST, taking care not to insert duplicate values.
let mut builder = fst::MapBuilder::memory();
let mut last_batch_start = 0;
for idx in 0..importables.len() {
if let Some(next_item) = importables.get(idx + 1) {
if cmp(&importables[last_batch_start], next_item) == Ordering::Equal {
continue;
}
}
let start = last_batch_start;
last_batch_start = idx + 1;
let key = fst_path(&importables[start].1.path);
builder.insert(key, start as u64).unwrap();
}
import_map.fst = fst::Map::new(builder.into_inner().unwrap()).unwrap();
import_map.importables = importables.iter().map(|(item, _)| **item).collect();
Arc::new(import_map)
}
/// Returns the `ModPath` needed to import/mention `item`, relative to this crate's root.
pub fn path_of(&self, item: ItemInNs) -> Option<&ModPath> {
Some(&self.map.get(&item)?.path)
}
pub fn import_info_for(&self, item: ItemInNs) -> Option<&ImportInfo> {
self.map.get(&item)
}
fn collect_trait_methods(&mut self, db: &dyn DefDatabase, tr: TraitId) {
let data = db.trait_data(tr);
for (name, item) in data.items.iter() {
self.assoc_map.entry(name.to_string().into()).or_default().push(*item);
}
}
}
impl PartialEq for ImportMap {
fn eq(&self, other: &Self) -> bool {
// `fst` and `importables` are built from `map`, so we don't need to compare them.
self.map == other.map
}
}
impl Eq for ImportMap {}
impl fmt::Debug for ImportMap {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut importable_paths: Vec<_> = self
.map
.iter()
.map(|(item, info)| {
let ns = match item {
ItemInNs::Types(_) => "t",
ItemInNs::Values(_) => "v",
ItemInNs::Macros(_) => "m",
};
format!("- {} ({})", info.path, ns)
})
.collect();
importable_paths.sort();
f.write_str(&importable_paths.join("\n"))
}
}
fn fst_path(path: &ModPath) -> String {
let mut s = path.to_string();
s.make_ascii_lowercase();
s
}
fn cmp((_, lhs): &(&ItemInNs, &ImportInfo), (_, rhs): &(&ItemInNs, &ImportInfo)) -> Ordering {
let lhs_str = fst_path(&lhs.path);
let rhs_str = fst_path(&rhs.path);
lhs_str.cmp(&rhs_str)
}
#[derive(Debug)]
pub struct Query {
query: String,
lowercased: String,
anchor_end: bool,
case_sensitive: bool,
limit: usize,
}
impl Query {
pub fn new(query: &str) -> Self {
Self {
lowercased: query.to_lowercase(),
query: query.to_string(),
anchor_end: false,
case_sensitive: false,
limit: usize::max_value(),
}
}
/// Only returns items whose paths end with the (case-insensitive) query string as their last
/// segment.
pub fn anchor_end(self) -> Self {
Self { anchor_end: true, ..self }
}
/// Limits the returned number of items to `limit`.
pub fn limit(self, limit: usize) -> Self {
Self { limit, ..self }
}
/// Respect casing of the query string when matching.
pub fn case_sensitive(self) -> Self {
Self { case_sensitive: true, ..self }
}
}
/// Searches dependencies of `krate` for an importable path matching `query`.
///
/// This returns a list of items that could be imported from dependencies of `krate`.
pub fn search_dependencies<'a>(
db: &'a dyn DefDatabase,
krate: CrateId,
query: Query,
) -> Vec<ItemInNs> {
let _p = profile::span("search_dependencies").detail(|| format!("{:?}", query));
let graph = db.crate_graph();
let import_maps: Vec<_> =
graph[krate].dependencies.iter().map(|dep| db.import_map(dep.crate_id)).collect();
let automaton = fst::automaton::Subsequence::new(&query.lowercased);
let mut op = fst::map::OpBuilder::new();
for map in &import_maps {
op = op.add(map.fst.search(&automaton));
}
let mut stream = op.union();
let mut res = Vec::new();
while let Some((_, indexed_values)) = stream.next() {
for indexed_value in indexed_values {
let import_map = &import_maps[indexed_value.index];
let importables = &import_map.importables[indexed_value.value as usize..];
// Path shared by the importable items in this group.
let path = &import_map.map[&importables[0]].path;
if query.anchor_end {
// Last segment must match query.
let last = path.segments.last().unwrap().to_string();
if last.to_lowercase() != query.lowercased {
continue;
}
}
// Add the items from this `ModPath` group. Those are all subsequent items in
// `importables` whose paths match `path`.
let iter = importables.iter().copied().take_while(|item| {
let item_path = &import_map.map[item].path;
fst_path(item_path) == fst_path(path)
});
if query.case_sensitive {
// FIXME: This does not do a subsequence match.
res.extend(iter.filter(|item| {
let item_path = &import_map.map[item].path;
item_path.to_string().contains(&query.query)
}));
} else {
res.extend(iter);
}
if res.len() >= query.limit {
res.truncate(query.limit);
return res;
}
}
}
// Add all exported associated items whose names match the query (exactly).
for map in &import_maps {
if let Some(v) = map.assoc_map.get(&*query.query) {
res.extend(v.iter().map(|&assoc| {
ItemInNs::Types(match assoc {
AssocItemId::FunctionId(it) => it.into(),
AssocItemId::ConstId(it) => it.into(),
AssocItemId::TypeAliasId(it) => it.into(),
})
}));
}
}
res
}
#[cfg(test)]
mod tests {
use base_db::{fixture::WithFixture, SourceDatabase, Upcast};
use expect_test::{expect, Expect};
use crate::{test_db::TestDB, AssocContainerId, Lookup};
use super::*;
fn check_search(ra_fixture: &str, krate_name: &str, query: Query, expect: Expect) {
let db = TestDB::with_files(ra_fixture);
let crate_graph = db.crate_graph();
let krate = crate_graph
.iter()
.find(|krate| {
crate_graph[*krate].display_name.as_ref().map(|n| n.to_string())
== Some(krate_name.to_string())
})
.unwrap();
let actual = search_dependencies(db.upcast(), krate, query)
.into_iter()
.filter_map(|item| {
let mark = match item {
ItemInNs::Types(_) => "t",
ItemInNs::Values(_) => "v",
ItemInNs::Macros(_) => "m",
};
let item = assoc_to_trait(&db, item);
item.krate(db.upcast()).map(|krate| {
let map = db.import_map(krate);
let path = map.path_of(item).unwrap();
format!(
"{}::{} ({})\n",
crate_graph[krate].display_name.as_ref().unwrap(),
path,
mark
)
})
})
.collect::<String>();
expect.assert_eq(&actual)
}
fn assoc_to_trait(db: &dyn DefDatabase, item: ItemInNs) -> ItemInNs {
let assoc: AssocItemId = match item {
ItemInNs::Types(it) | ItemInNs::Values(it) => match it {
ModuleDefId::TypeAliasId(it) => it.into(),
ModuleDefId::FunctionId(it) => it.into(),
ModuleDefId::ConstId(it) => it.into(),
_ => return item,
},
_ => return item,
};
let container = match assoc {
AssocItemId::FunctionId(it) => it.lookup(db).container,
AssocItemId::ConstId(it) => it.lookup(db).container,
AssocItemId::TypeAliasId(it) => it.lookup(db).container,
};
match container {
AssocContainerId::TraitId(it) => ItemInNs::Types(it.into()),
_ => item,
}
}
fn check(ra_fixture: &str, expect: Expect) {
let db = TestDB::with_files(ra_fixture);
let crate_graph = db.crate_graph();
let actual = crate_graph
.iter()
.filter_map(|krate| {
let cdata = &crate_graph[krate];
let name = cdata.display_name.as_ref()?;
let map = db.import_map(krate);
Some(format!("{}:\n{:?}\n", name, map))
})
.collect::<String>();
expect.assert_eq(&actual)
}
#[test]
fn smoke() {
check(
r"
//- /main.rs crate:main deps:lib
mod private {
pub use lib::Pub;
pub struct InPrivateModule;
}
pub mod publ1 {
use lib::Pub;
}
pub mod real_pub {
pub use lib::Pub;
}
pub mod real_pu2 { // same path length as above
pub use lib::Pub;
}
//- /lib.rs crate:lib
pub struct Pub {}
pub struct Pub2; // t + v
struct Priv;
",
expect![[r#"
main:
- publ1 (t)
- real_pu2 (t)
- real_pub (t)
- real_pub::Pub (t)
lib:
- Pub (t)
- Pub2 (t)
- Pub2 (v)
"#]],
);
}
#[test]
fn prefers_shortest_path() {
check(
r"
//- /main.rs crate:main
pub mod sub {
pub mod subsub {
pub struct Def {}
}
pub use super::sub::subsub::Def;
}
",
expect![[r#"
main:
- sub (t)
- sub::Def (t)
- sub::subsub (t)
"#]],
);
}
#[test]
fn type_reexport_cross_crate() {
// Reexports need to be visible from a crate, even if the original crate exports the item
// at a shorter path.
check(
r"
//- /main.rs crate:main deps:lib
pub mod m {
pub use lib::S;
}
//- /lib.rs crate:lib
pub struct S;
",
expect![[r#"
main:
- m (t)
- m::S (t)
- m::S (v)
lib:
- S (t)
- S (v)
"#]],
);
}
#[test]
fn macro_reexport() {
check(
r"
//- /main.rs crate:main deps:lib
pub mod m {
pub use lib::pub_macro;
}
//- /lib.rs crate:lib
#[macro_export]
macro_rules! pub_macro {
() => {};
}
",
expect![[r#"
main:
- m (t)
- m::pub_macro (m)
lib:
- pub_macro (m)
"#]],
);
}
#[test]
fn module_reexport() {
// Reexporting modules from a dependency adds all contents to the import map.
check(
r"
//- /main.rs crate:main deps:lib
pub use lib::module as reexported_module;
//- /lib.rs crate:lib
pub mod module {
pub struct S;
}
",
expect![[r#"
main:
- reexported_module (t)
- reexported_module::S (t)
- reexported_module::S (v)
lib:
- module (t)
- module::S (t)
- module::S (v)
"#]],
);
}
#[test]
fn cyclic_module_reexport() {
// A cyclic reexport does not hang.
check(
r"
//- /lib.rs crate:lib
pub mod module {
pub struct S;
pub use super::sub::*;
}
pub mod sub {
pub use super::module;
}
",
expect![[r#"
lib:
- module (t)
- module::S (t)
- module::S (v)
- sub (t)
"#]],
);
}
#[test]
fn private_macro() {
check(
r"
//- /lib.rs crate:lib
macro_rules! private_macro {
() => {};
}
",
expect![[r#"
lib:
"#]],
);
}
#[test]
fn namespacing() {
check(
r"
//- /lib.rs crate:lib
pub struct Thing; // t + v
#[macro_export]
macro_rules! Thing { // m
() => {};
}
",
expect![[r#"
lib:
- Thing (m)
- Thing (t)
- Thing (v)
"#]],
);
check(
r"
//- /lib.rs crate:lib
pub mod Thing {} // t
#[macro_export]
macro_rules! Thing { // m
() => {};
}
",
expect![[r#"
lib:
- Thing (m)
- Thing (t)
"#]],
);
}
#[test]
fn search() {
let ra_fixture = r#"
//- /main.rs crate:main deps:dep
//- /dep.rs crate:dep deps:tdep
use tdep::fmt as fmt_dep;
pub mod fmt {
pub trait Display {
fn fmt();
}
}
#[macro_export]
macro_rules! Fmt {
() => {};
}
pub struct Fmt;
pub fn format() {}
pub fn no() {}
//- /tdep.rs crate:tdep
pub mod fmt {
pub struct NotImportableFromMain;
}
"#;
check_search(
ra_fixture,
"main",
Query::new("fmt"),
expect![[r#"
dep::fmt (t)
dep::Fmt (t)
dep::Fmt (v)
dep::Fmt (m)
dep::fmt::Display (t)
dep::format (v)
dep::fmt::Display (t)
"#]],
);
check_search(
ra_fixture,
"main",
Query::new("fmt").anchor_end(),
expect![[r#"
dep::fmt (t)
dep::Fmt (t)
dep::Fmt (v)
dep::Fmt (m)
dep::fmt::Display (t)
"#]],
);
}
#[test]
fn search_casing() {
let ra_fixture = r#"
//- /main.rs crate:main deps:dep
//- /dep.rs crate:dep
pub struct fmt;
pub struct FMT;
"#;
check_search(
ra_fixture,
"main",
Query::new("FMT"),
expect![[r#"
dep::fmt (t)
dep::fmt (v)
dep::FMT (t)
dep::FMT (v)
"#]],
);
check_search(
ra_fixture,
"main",
Query::new("FMT").case_sensitive(),
expect![[r#"
dep::FMT (t)
dep::FMT (v)
"#]],
);
}
#[test]
fn search_limit() {
check_search(
r#"
//- /main.rs crate:main deps:dep
//- /dep.rs crate:dep
pub mod fmt {
pub trait Display {
fn fmt();
}
}
#[macro_export]
macro_rules! Fmt {
() => {};
}
pub struct Fmt;
pub fn format() {}
pub fn no() {}
"#,
"main",
Query::new("").limit(2),
expect![[r#"
dep::fmt (t)
dep::Fmt (t)
"#]],
);
}
}

Some files were not shown because too many files have changed in this diff Show more