mirror of
https://github.com/rust-lang/rust-analyzer
synced 2024-11-16 01:38:13 +00:00
Merge #403
403: initial support for macros r=matklad a=matklad I'll write a more comprehensive description when this is closer to being done. Basically this investigates one question: "how do we represent code which is a result of a macro call". This is an interesting question: currently everything is `FileId` based, but macro expansion does not have a file! Co-authored-by: Aleksey Kladov <aleksey.kladov@gmail.com>
This commit is contained in:
commit
afa972e78d
22 changed files with 778 additions and 344 deletions
|
@ -27,7 +27,7 @@ pub(super) fn complete_scope(acc: &mut Completions, ctx: &CompletionContext) ->
|
|||
match res.import {
|
||||
None => true,
|
||||
Some(import) => {
|
||||
let range = import.range(ctx.db, module.source().file_id());
|
||||
let range = import.range(ctx.db, module.file_id());
|
||||
!range.is_subrange(&ctx.leaf.range())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
use std::{fmt, sync::Arc};
|
||||
use salsa::{self, Database};
|
||||
use ra_db::{LocationIntener, BaseDatabase};
|
||||
use hir::{self, DefId, DefLoc};
|
||||
|
||||
use crate::{
|
||||
symbol_index,
|
||||
|
@ -15,7 +14,8 @@ pub(crate) struct RootDatabase {
|
|||
|
||||
#[derive(Default)]
|
||||
struct IdMaps {
|
||||
defs: LocationIntener<DefLoc, DefId>,
|
||||
defs: LocationIntener<hir::DefLoc, hir::DefId>,
|
||||
macros: LocationIntener<hir::MacroCallLoc, hir::MacroCallId>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for IdMaps {
|
||||
|
@ -59,12 +59,18 @@ impl salsa::ParallelDatabase for RootDatabase {
|
|||
|
||||
impl BaseDatabase for RootDatabase {}
|
||||
|
||||
impl AsRef<LocationIntener<DefLoc, DefId>> for RootDatabase {
|
||||
fn as_ref(&self) -> &LocationIntener<DefLoc, DefId> {
|
||||
impl AsRef<LocationIntener<hir::DefLoc, hir::DefId>> for RootDatabase {
|
||||
fn as_ref(&self) -> &LocationIntener<hir::DefLoc, hir::DefId> {
|
||||
&self.id_maps.defs
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<LocationIntener<hir::MacroCallLoc, hir::MacroCallId>> for RootDatabase {
|
||||
fn as_ref(&self) -> &LocationIntener<hir::MacroCallLoc, hir::MacroCallId> {
|
||||
&self.id_maps.macros
|
||||
}
|
||||
}
|
||||
|
||||
salsa::database_storage! {
|
||||
pub(crate) struct RootDatabaseStorage for RootDatabase {
|
||||
impl ra_db::FilesDatabase {
|
||||
|
@ -85,6 +91,8 @@ salsa::database_storage! {
|
|||
fn library_symbols() for symbol_index::LibrarySymbolsQuery;
|
||||
}
|
||||
impl hir::db::HirDatabase {
|
||||
fn hir_source_file() for hir::db::HirSourceFileQuery;
|
||||
fn expand_macro_invocation() for hir::db::ExpandMacroCallQuery;
|
||||
fn module_tree() for hir::db::ModuleTreeQuery;
|
||||
fn fn_scopes() for hir::db::FnScopesQuery;
|
||||
fn file_items() for hir::db::SourceFileItemsQuery;
|
||||
|
|
|
@ -18,15 +18,15 @@ pub(crate) fn extend_selection(db: &RootDatabase, frange: FileRange) -> TextRang
|
|||
}
|
||||
|
||||
fn extend_selection_in_macro(
|
||||
db: &RootDatabase,
|
||||
_db: &RootDatabase,
|
||||
source_file: &SourceFileNode,
|
||||
frange: FileRange,
|
||||
) -> Option<TextRange> {
|
||||
let macro_call = find_macro_call(source_file.syntax(), frange.range)?;
|
||||
let exp = crate::macros::expand(db, frange.file_id, macro_call)?;
|
||||
let dst_range = exp.map_range_forward(frange.range)?;
|
||||
let dst_range = ra_editor::extend_selection(exp.source_file().syntax(), dst_range)?;
|
||||
let src_range = exp.map_range_back(dst_range)?;
|
||||
let (off, exp) = hir::MacroDef::ast_expand(macro_call)?;
|
||||
let dst_range = exp.map_range_forward(frange.range - off)?;
|
||||
let dst_range = ra_editor::extend_selection(exp.syntax().borrowed(), dst_range)?;
|
||||
let src_range = exp.map_range_back(dst_range)? + off;
|
||||
Some(src_range)
|
||||
}
|
||||
|
||||
|
|
|
@ -230,7 +230,7 @@ impl AnalysisImpl {
|
|||
Some(it) => it,
|
||||
};
|
||||
let root = descr.crate_root();
|
||||
let file_id = root.source().file_id();
|
||||
let file_id = root.file_id();
|
||||
|
||||
let crate_graph = self.db.crate_graph();
|
||||
let crate_id = crate_graph.crate_id_for_crate_root(file_id);
|
||||
|
@ -283,7 +283,7 @@ impl AnalysisImpl {
|
|||
if let Some(child_module) =
|
||||
source_binder::module_from_declaration(&*self.db, position.file_id, module)?
|
||||
{
|
||||
let file_id = child_module.source().file_id();
|
||||
let file_id = child_module.file_id();
|
||||
let name = match child_module.name() {
|
||||
Some(name) => name.to_string().into(),
|
||||
None => "".into(),
|
||||
|
|
|
@ -19,7 +19,6 @@ mod runnables;
|
|||
|
||||
mod extend_selection;
|
||||
mod syntax_highlighting;
|
||||
mod macros;
|
||||
|
||||
use std::{fmt, sync::Arc};
|
||||
|
||||
|
|
|
@ -1,75 +0,0 @@
|
|||
/// Begining of macro expansion.
|
||||
///
|
||||
/// This code should be moved out of ra_analysis into hir (?) ideally.
|
||||
use ra_syntax::{ast, AstNode, SourceFileNode, TextRange};
|
||||
|
||||
use crate::{db::RootDatabase, FileId};
|
||||
|
||||
pub(crate) fn expand(
|
||||
_db: &RootDatabase,
|
||||
_file_id: FileId,
|
||||
macro_call: ast::MacroCall,
|
||||
) -> Option<MacroExpansion> {
|
||||
let path = macro_call.path()?;
|
||||
if path.qualifier().is_some() {
|
||||
return None;
|
||||
}
|
||||
let name_ref = path.segment()?.name_ref()?;
|
||||
if name_ref.text() != "ctry" {
|
||||
return None;
|
||||
}
|
||||
|
||||
let arg = macro_call.token_tree()?;
|
||||
let text = format!(
|
||||
r"
|
||||
fn dummy() {{
|
||||
match {} {{
|
||||
None => return Ok(None),
|
||||
Some(it) => it,
|
||||
}}
|
||||
}}",
|
||||
arg.syntax().text()
|
||||
);
|
||||
let file = SourceFileNode::parse(&text);
|
||||
let match_expr = file.syntax().descendants().find_map(ast::MatchExpr::cast)?;
|
||||
let match_arg = match_expr.expr()?;
|
||||
let ranges_map = vec![(arg.syntax().range(), match_arg.syntax().range())];
|
||||
let res = MacroExpansion {
|
||||
source_file: file,
|
||||
ranges_map,
|
||||
};
|
||||
Some(res)
|
||||
}
|
||||
|
||||
pub(crate) struct MacroExpansion {
|
||||
pub(crate) source_file: SourceFileNode,
|
||||
pub(crate) ranges_map: Vec<(TextRange, TextRange)>,
|
||||
}
|
||||
|
||||
impl MacroExpansion {
|
||||
pub(crate) fn source_file(&self) -> &SourceFileNode {
|
||||
&self.source_file
|
||||
}
|
||||
pub(crate) fn map_range_back(&self, tgt_range: TextRange) -> Option<TextRange> {
|
||||
for (s_range, t_range) in self.ranges_map.iter() {
|
||||
if tgt_range.is_subrange(&t_range) {
|
||||
let tgt_at_zero_range = tgt_range - tgt_range.start();
|
||||
let tgt_range_offset = tgt_range.start() - t_range.start();
|
||||
let src_range = tgt_at_zero_range + tgt_range_offset + s_range.start();
|
||||
return Some(src_range);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
pub(crate) fn map_range_forward(&self, src_range: TextRange) -> Option<TextRange> {
|
||||
for (s_range, t_range) in self.ranges_map.iter() {
|
||||
if src_range.is_subrange(&s_range) {
|
||||
let src_at_zero_range = src_range - src_range.start();
|
||||
let src_range_offset = src_range.start() - s_range.start();
|
||||
let src_range = src_at_zero_range + src_range_offset + t_range.start();
|
||||
return Some(src_range);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
|
@ -15,13 +15,13 @@ pub(crate) fn highlight(db: &RootDatabase, file_id: FileId) -> Cancelable<Vec<Hi
|
|||
.descendants()
|
||||
.filter_map(ast::MacroCall::cast)
|
||||
{
|
||||
if let Some(exp) = crate::macros::expand(db, file_id, macro_call) {
|
||||
let mapped_ranges = ra_editor::highlight(exp.source_file().syntax())
|
||||
if let Some((off, exp)) = hir::MacroDef::ast_expand(macro_call) {
|
||||
let mapped_ranges = ra_editor::highlight(exp.syntax().borrowed())
|
||||
.into_iter()
|
||||
.filter_map(|r| {
|
||||
let mapped_range = exp.map_range_back(r.range)?;
|
||||
let res = HighlightedRange {
|
||||
range: mapped_range,
|
||||
range: mapped_range + off,
|
||||
tag: r.tag,
|
||||
};
|
||||
Some(res)
|
||||
|
@ -44,7 +44,7 @@ mod tests {
|
|||
fn main() {
|
||||
ctry!({ let x = 92; x});
|
||||
}
|
||||
",
|
||||
",
|
||||
);
|
||||
let highlights = analysis.highlight(file_id).unwrap();
|
||||
assert_eq_dbg(
|
||||
|
@ -60,4 +60,26 @@ mod tests {
|
|||
&highlights,
|
||||
)
|
||||
}
|
||||
|
||||
// FIXME: this test is not really necessary: artifact of the inital hacky
|
||||
// macros implementation.
|
||||
#[test]
|
||||
fn highlight_query_group_macro() {
|
||||
let (analysis, file_id) = single_file(
|
||||
"
|
||||
salsa::query_group! {
|
||||
pub trait HirDatabase: SyntaxDatabase {}
|
||||
}
|
||||
",
|
||||
);
|
||||
let highlights = analysis.highlight(file_id).unwrap();
|
||||
assert_eq_dbg(
|
||||
r#"[HighlightedRange { range: [20; 32), tag: "macro" },
|
||||
HighlightedRange { range: [13; 18), tag: "text" },
|
||||
HighlightedRange { range: [51; 54), tag: "keyword" },
|
||||
HighlightedRange { range: [55; 60), tag: "keyword" },
|
||||
HighlightedRange { range: [61; 72), tag: "function" }]"#,
|
||||
&highlights,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use ra_db::Cancelable;
|
||||
use ra_syntax::ast::{self, NameOwner, StructFlavor};
|
||||
|
||||
use crate::{
|
||||
DefId, Cancelable, Name, AsName,
|
||||
DefId, Name, AsName,
|
||||
db::HirDatabase,
|
||||
type_ref::TypeRef,
|
||||
};
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use ra_syntax::SyntaxNode;
|
||||
use ra_db::{SourceRootId, LocationIntener, SyntaxDatabase, FileId, Cancelable};
|
||||
use ra_syntax::{SyntaxNode, SourceFileNode};
|
||||
use ra_db::{SourceRootId, LocationIntener, SyntaxDatabase, Cancelable};
|
||||
|
||||
use crate::{
|
||||
DefLoc, DefId, Name,
|
||||
DefLoc, DefId, MacroCallLoc, MacroCallId, Name, HirFileId,
|
||||
SourceFileItems, SourceItemId,
|
||||
query_definitions,
|
||||
FnScopes,
|
||||
macros::MacroExpansion,
|
||||
module::{ModuleId, ModuleTree, ModuleSource,
|
||||
nameres::{ItemMap, InputModuleItems}},
|
||||
ty::{InferenceResult, Ty},
|
||||
|
@ -18,7 +19,17 @@ salsa::query_group! {
|
|||
|
||||
pub trait HirDatabase: SyntaxDatabase
|
||||
+ AsRef<LocationIntener<DefLoc, DefId>>
|
||||
+ AsRef<LocationIntener<MacroCallLoc, MacroCallId>>
|
||||
{
|
||||
fn hir_source_file(file_id: HirFileId) -> SourceFileNode {
|
||||
type HirSourceFileQuery;
|
||||
use fn HirFileId::hir_source_file;
|
||||
}
|
||||
fn expand_macro_invocation(invoc: MacroCallId) -> Option<Arc<MacroExpansion>> {
|
||||
type ExpandMacroCallQuery;
|
||||
use fn crate::macros::expand_macro_invocation;
|
||||
}
|
||||
|
||||
fn fn_scopes(def_id: DefId) -> Arc<FnScopes> {
|
||||
type FnScopesQuery;
|
||||
use fn query_definitions::fn_scopes;
|
||||
|
@ -49,7 +60,7 @@ pub trait HirDatabase: SyntaxDatabase
|
|||
use fn crate::ty::type_for_field;
|
||||
}
|
||||
|
||||
fn file_items(file_id: FileId) -> Arc<SourceFileItems> {
|
||||
fn file_items(file_id: HirFileId) -> Arc<SourceFileItems> {
|
||||
type SourceFileItemsQuery;
|
||||
use fn query_definitions::file_items;
|
||||
}
|
||||
|
|
280
crates/ra_hir/src/ids.rs
Normal file
280
crates/ra_hir/src/ids.rs
Normal file
|
@ -0,0 +1,280 @@
|
|||
use ra_db::{SourceRootId, LocationIntener, Cancelable, FileId};
|
||||
use ra_syntax::{SourceFileNode, SyntaxKind, SyntaxNode, SyntaxNodeRef, SourceFile, AstNode, ast};
|
||||
|
||||
use crate::{
|
||||
HirDatabase, PerNs, ModuleId, Module, Def, Function, Struct, Enum,
|
||||
arena::{Arena, Id},
|
||||
};
|
||||
|
||||
/// hir makes a heavy use of ids: integer (u32) handlers to various things. You
|
||||
/// can think of id as a pointer (but without a lifetime) or a file descriptor
|
||||
/// (but for hir objects).
|
||||
///
|
||||
/// This module defines a bunch of ids we are using. The most important ones are
|
||||
/// probably `HirFileId` and `DefId`.
|
||||
|
||||
/// Input to the analyzer is a set of file, where each file is indetified by
|
||||
/// `FileId` and contains source code. However, another source of source code in
|
||||
/// Rust are macros: each macro can be thought of as producing a "temporary
|
||||
/// file". To assign id to such file, we use the id of a macro call that
|
||||
/// produced the file. So, a `HirFileId` is either a `FileId` (source code
|
||||
/// written by user), or a `MacroCallId` (source code produced by macro).
|
||||
///
|
||||
/// What is a `MacroCallId`? Simplifying, it's a `HirFileId` of a file containin
|
||||
/// the call plus the offset of the macro call in the file. Note that this is a
|
||||
/// recursive definition! Nethetheless, size_of of `HirFileId` is finite
|
||||
/// (because everything bottoms out at the real `FileId`) and small
|
||||
/// (`MacroCallId` uses location interner).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct HirFileId(HirFileIdRepr);
|
||||
|
||||
impl HirFileId {
|
||||
/// For macro-expansion files, returns the file original source file the
|
||||
/// expansionoriginated from.
|
||||
pub(crate) fn original_file(self, db: &impl HirDatabase) -> FileId {
|
||||
match self.0 {
|
||||
HirFileIdRepr::File(file_id) => file_id,
|
||||
HirFileIdRepr::Macro(macro_call_id) => {
|
||||
let loc = macro_call_id.loc(db);
|
||||
loc.source_item_id.file_id.original_file(db)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn as_original_file(self) -> FileId {
|
||||
match self.0 {
|
||||
HirFileIdRepr::File(file_id) => file_id,
|
||||
HirFileIdRepr::Macro(_r) => panic!("macro generated file: {:?}", self),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn hir_source_file(db: &impl HirDatabase, file_id: HirFileId) -> SourceFileNode {
|
||||
match file_id.0 {
|
||||
HirFileIdRepr::File(file_id) => db.source_file(file_id),
|
||||
HirFileIdRepr::Macro(m) => {
|
||||
if let Some(exp) = db.expand_macro_invocation(m) {
|
||||
return exp.file();
|
||||
}
|
||||
// returning an empty string looks fishy...
|
||||
SourceFileNode::parse("")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
enum HirFileIdRepr {
|
||||
File(FileId),
|
||||
Macro(MacroCallId),
|
||||
}
|
||||
|
||||
impl From<FileId> for HirFileId {
|
||||
fn from(file_id: FileId) -> HirFileId {
|
||||
HirFileId(HirFileIdRepr::File(file_id))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MacroCallId> for HirFileId {
|
||||
fn from(macro_call_id: MacroCallId) -> HirFileId {
|
||||
HirFileId(HirFileIdRepr::Macro(macro_call_id))
|
||||
}
|
||||
}
|
||||
|
||||
/// `MacroCallId` identifies a particular macro invocation, like
|
||||
/// `println!("Hello, {}", world)`.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct MacroCallId(u32);
|
||||
ra_db::impl_numeric_id!(MacroCallId);
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct MacroCallLoc {
|
||||
pub(crate) source_root_id: SourceRootId,
|
||||
pub(crate) module_id: ModuleId,
|
||||
pub(crate) source_item_id: SourceItemId,
|
||||
}
|
||||
|
||||
impl MacroCallId {
|
||||
pub(crate) fn loc(
|
||||
self,
|
||||
db: &impl AsRef<LocationIntener<MacroCallLoc, MacroCallId>>,
|
||||
) -> MacroCallLoc {
|
||||
db.as_ref().id2loc(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl MacroCallLoc {
|
||||
#[allow(unused)]
|
||||
pub(crate) fn id(
|
||||
&self,
|
||||
db: &impl AsRef<LocationIntener<MacroCallLoc, MacroCallId>>,
|
||||
) -> MacroCallId {
|
||||
db.as_ref().loc2id(&self)
|
||||
}
|
||||
}
|
||||
|
||||
/// Def's are a core concept of hir. A `Def` is an Item (function, module, etc)
|
||||
/// in a specific module.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct DefId(u32);
|
||||
ra_db::impl_numeric_id!(DefId);
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct DefLoc {
|
||||
pub(crate) kind: DefKind,
|
||||
pub(crate) source_root_id: SourceRootId,
|
||||
pub(crate) module_id: ModuleId,
|
||||
pub(crate) source_item_id: SourceItemId,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub(crate) enum DefKind {
|
||||
Module,
|
||||
Function,
|
||||
Struct,
|
||||
Enum,
|
||||
Item,
|
||||
|
||||
StructCtor,
|
||||
}
|
||||
|
||||
impl DefId {
|
||||
pub(crate) fn loc(self, db: &impl AsRef<LocationIntener<DefLoc, DefId>>) -> DefLoc {
|
||||
db.as_ref().id2loc(self)
|
||||
}
|
||||
|
||||
pub fn resolve(self, db: &impl HirDatabase) -> Cancelable<Def> {
|
||||
let loc = self.loc(db);
|
||||
let res = match loc.kind {
|
||||
DefKind::Module => {
|
||||
let module = Module::new(db, loc.source_root_id, loc.module_id)?;
|
||||
Def::Module(module)
|
||||
}
|
||||
DefKind::Function => {
|
||||
let function = Function::new(self);
|
||||
Def::Function(function)
|
||||
}
|
||||
DefKind::Struct => {
|
||||
let struct_def = Struct::new(self);
|
||||
Def::Struct(struct_def)
|
||||
}
|
||||
DefKind::Enum => {
|
||||
let enum_def = Enum::new(self);
|
||||
Def::Enum(enum_def)
|
||||
}
|
||||
DefKind::StructCtor => Def::Item,
|
||||
DefKind::Item => Def::Item,
|
||||
};
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// For a module, returns that module; for any other def, returns the containing module.
|
||||
pub fn module(self, db: &impl HirDatabase) -> Cancelable<Module> {
|
||||
let loc = self.loc(db);
|
||||
Module::new(db, loc.source_root_id, loc.module_id)
|
||||
}
|
||||
}
|
||||
|
||||
impl DefLoc {
|
||||
pub(crate) fn id(&self, db: &impl AsRef<LocationIntener<DefLoc, DefId>>) -> DefId {
|
||||
db.as_ref().loc2id(&self)
|
||||
}
|
||||
}
|
||||
|
||||
impl DefKind {
|
||||
pub(crate) fn for_syntax_kind(kind: SyntaxKind) -> PerNs<DefKind> {
|
||||
match kind {
|
||||
SyntaxKind::FN_DEF => PerNs::values(DefKind::Function),
|
||||
SyntaxKind::MODULE => PerNs::types(DefKind::Module),
|
||||
SyntaxKind::STRUCT_DEF => PerNs::both(DefKind::Struct, DefKind::StructCtor),
|
||||
SyntaxKind::ENUM_DEF => PerNs::types(DefKind::Enum),
|
||||
// These define items, but don't have their own DefKinds yet:
|
||||
SyntaxKind::TRAIT_DEF => PerNs::types(DefKind::Item),
|
||||
SyntaxKind::TYPE_DEF => PerNs::types(DefKind::Item),
|
||||
SyntaxKind::CONST_DEF => PerNs::values(DefKind::Item),
|
||||
SyntaxKind::STATIC_DEF => PerNs::values(DefKind::Item),
|
||||
_ => PerNs::none(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Identifier of item within a specific file. This is stable over reparses, so
|
||||
/// it's OK to use it as a salsa key/value.
|
||||
pub(crate) type SourceFileItemId = Id<SyntaxNode>;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct SourceItemId {
|
||||
pub(crate) file_id: HirFileId,
|
||||
/// None for the whole file.
|
||||
pub(crate) item_id: Option<SourceFileItemId>,
|
||||
}
|
||||
|
||||
/// Maps item's `SyntaxNode`s to `SourceFileItemId` and back.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct SourceFileItems {
|
||||
file_id: HirFileId,
|
||||
arena: Arena<SyntaxNode>,
|
||||
}
|
||||
|
||||
impl SourceFileItems {
|
||||
pub(crate) fn new(file_id: HirFileId, source_file: SourceFile) -> SourceFileItems {
|
||||
let mut res = SourceFileItems {
|
||||
file_id,
|
||||
arena: Arena::default(),
|
||||
};
|
||||
res.init(source_file);
|
||||
res
|
||||
}
|
||||
|
||||
fn init(&mut self, source_file: SourceFile) {
|
||||
source_file.syntax().descendants().for_each(|it| {
|
||||
if let Some(module_item) = ast::ModuleItem::cast(it) {
|
||||
self.alloc(module_item.syntax().owned());
|
||||
} else if let Some(macro_call) = ast::MacroCall::cast(it) {
|
||||
self.alloc(macro_call.syntax().owned());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn alloc(&mut self, item: SyntaxNode) -> SourceFileItemId {
|
||||
self.arena.alloc(item)
|
||||
}
|
||||
pub(crate) fn id_of(&self, file_id: HirFileId, item: SyntaxNodeRef) -> SourceFileItemId {
|
||||
assert_eq!(
|
||||
self.file_id, file_id,
|
||||
"SourceFileItems: wrong file, expected {:?}, got {:?}",
|
||||
self.file_id, file_id
|
||||
);
|
||||
self.id_of_unchecked(item)
|
||||
}
|
||||
pub(crate) fn id_of_unchecked(&self, item: SyntaxNodeRef) -> SourceFileItemId {
|
||||
if let Some((id, _)) = self.arena.iter().find(|(_id, i)| i.borrowed() == item) {
|
||||
return id;
|
||||
}
|
||||
// This should not happen. Let's try to give a sensible diagnostics.
|
||||
if let Some((id, i)) = self.arena.iter().find(|(_id, i)| i.range() == item.range()) {
|
||||
// FIXME(#288): whyyy are we getting here?
|
||||
log::error!(
|
||||
"unequal syntax nodes with the same range:\n{:?}\n{:?}",
|
||||
item,
|
||||
i
|
||||
);
|
||||
return id;
|
||||
}
|
||||
panic!(
|
||||
"Can't find {:?} in SourceFileItems:\n{:?}",
|
||||
item,
|
||||
self.arena.iter().map(|(_id, i)| i).collect::<Vec<_>>(),
|
||||
);
|
||||
}
|
||||
pub fn id_of_source_file(&self) -> SourceFileItemId {
|
||||
let (id, _syntax) = self.arena.iter().next().unwrap();
|
||||
id
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Index<SourceFileItemId> for SourceFileItems {
|
||||
type Output = SyntaxNode;
|
||||
fn index(&self, idx: SourceFileItemId) -> &SyntaxNode {
|
||||
&self.arena[idx]
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
pub use ra_db::CrateId;
|
||||
pub use ra_db::{CrateId, Cancelable};
|
||||
|
||||
use crate::{HirDatabase, Module, Cancelable, Name, AsName};
|
||||
use crate::{HirDatabase, Module, Name, AsName, HirFileId};
|
||||
|
||||
/// hir::Crate describes a single crate. It's the main inteface with which
|
||||
/// crate's dependencies interact. Mostly, it should be just a proxy for the
|
||||
|
@ -35,6 +35,7 @@ impl Crate {
|
|||
let crate_graph = db.crate_graph();
|
||||
let file_id = crate_graph.crate_root(self.crate_id);
|
||||
let source_root_id = db.file_source_root(file_id);
|
||||
let file_id = HirFileId::from(file_id);
|
||||
let module_tree = db.module_tree(source_root_id)?;
|
||||
// FIXME: teach module tree about crate roots instead of guessing
|
||||
let (module_id, _) = ctry!(module_tree
|
||||
|
|
|
@ -22,7 +22,10 @@ mod path;
|
|||
mod arena;
|
||||
pub mod source_binder;
|
||||
|
||||
mod ids;
|
||||
mod macros;
|
||||
mod name;
|
||||
// can't use `crate` or `r#crate` here :(
|
||||
mod krate;
|
||||
mod module;
|
||||
mod function;
|
||||
|
@ -30,21 +33,18 @@ mod adt;
|
|||
mod type_ref;
|
||||
mod ty;
|
||||
|
||||
use std::ops::Index;
|
||||
|
||||
use ra_syntax::{SyntaxNodeRef, SyntaxNode, SyntaxKind};
|
||||
use ra_db::{LocationIntener, SourceRootId, FileId, Cancelable};
|
||||
|
||||
use crate::{
|
||||
db::HirDatabase,
|
||||
arena::{Arena, Id},
|
||||
name::{AsName, KnownName},
|
||||
ids::{DefKind, SourceItemId, SourceFileItemId, SourceFileItems},
|
||||
};
|
||||
|
||||
pub use self::{
|
||||
path::{Path, PathKind},
|
||||
name::Name,
|
||||
krate::Crate,
|
||||
ids::{HirFileId, DefId, DefLoc, MacroCallId, MacroCallLoc},
|
||||
macros::{MacroDef, MacroInput, MacroExpansion},
|
||||
module::{Module, ModuleId, Problem, nameres::{ItemMap, PerNs, Namespace}, ModuleScope, Resolution},
|
||||
function::{Function, FnScopes},
|
||||
adt::{Struct, Enum},
|
||||
|
@ -53,60 +53,6 @@ pub use self::{
|
|||
|
||||
pub use self::function::FnSignatureInfo;
|
||||
|
||||
/// Def's are a core concept of hir. A `Def` is an Item (function, module, etc)
|
||||
/// in a specific module.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct DefId(u32);
|
||||
ra_db::impl_numeric_id!(DefId);
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub(crate) enum DefKind {
|
||||
Module,
|
||||
Function,
|
||||
Struct,
|
||||
Enum,
|
||||
Item,
|
||||
|
||||
StructCtor,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct DefLoc {
|
||||
pub(crate) kind: DefKind,
|
||||
source_root_id: SourceRootId,
|
||||
module_id: ModuleId,
|
||||
source_item_id: SourceItemId,
|
||||
}
|
||||
|
||||
impl DefKind {
|
||||
pub(crate) fn for_syntax_kind(kind: SyntaxKind) -> PerNs<DefKind> {
|
||||
match kind {
|
||||
SyntaxKind::FN_DEF => PerNs::values(DefKind::Function),
|
||||
SyntaxKind::MODULE => PerNs::types(DefKind::Module),
|
||||
SyntaxKind::STRUCT_DEF => PerNs::both(DefKind::Struct, DefKind::StructCtor),
|
||||
SyntaxKind::ENUM_DEF => PerNs::types(DefKind::Enum),
|
||||
// These define items, but don't have their own DefKinds yet:
|
||||
SyntaxKind::TRAIT_DEF => PerNs::types(DefKind::Item),
|
||||
SyntaxKind::TYPE_DEF => PerNs::types(DefKind::Item),
|
||||
SyntaxKind::CONST_DEF => PerNs::values(DefKind::Item),
|
||||
SyntaxKind::STATIC_DEF => PerNs::values(DefKind::Item),
|
||||
_ => PerNs::none(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DefId {
|
||||
pub(crate) fn loc(self, db: &impl AsRef<LocationIntener<DefLoc, DefId>>) -> DefLoc {
|
||||
db.as_ref().id2loc(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl DefLoc {
|
||||
pub(crate) fn id(&self, db: &impl AsRef<LocationIntener<DefLoc, DefId>>) -> DefId {
|
||||
db.as_ref().loc2id(&self)
|
||||
}
|
||||
}
|
||||
|
||||
pub enum Def {
|
||||
Module(Module),
|
||||
Function(Function),
|
||||
|
@ -114,106 +60,3 @@ pub enum Def {
|
|||
Enum(Enum),
|
||||
Item,
|
||||
}
|
||||
|
||||
impl DefId {
|
||||
pub fn resolve(self, db: &impl HirDatabase) -> Cancelable<Def> {
|
||||
let loc = self.loc(db);
|
||||
let res = match loc.kind {
|
||||
DefKind::Module => {
|
||||
let module = Module::new(db, loc.source_root_id, loc.module_id)?;
|
||||
Def::Module(module)
|
||||
}
|
||||
DefKind::Function => {
|
||||
let function = Function::new(self);
|
||||
Def::Function(function)
|
||||
}
|
||||
DefKind::Struct => {
|
||||
let struct_def = Struct::new(self);
|
||||
Def::Struct(struct_def)
|
||||
}
|
||||
DefKind::Enum => {
|
||||
let enum_def = Enum::new(self);
|
||||
Def::Enum(enum_def)
|
||||
}
|
||||
DefKind::StructCtor => Def::Item,
|
||||
DefKind::Item => Def::Item,
|
||||
};
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// For a module, returns that module; for any other def, returns the containing module.
|
||||
pub fn module(self, db: &impl HirDatabase) -> Cancelable<Module> {
|
||||
let loc = self.loc(db);
|
||||
Module::new(db, loc.source_root_id, loc.module_id)
|
||||
}
|
||||
}
|
||||
|
||||
/// Identifier of item within a specific file. This is stable over reparses, so
|
||||
/// it's OK to use it as a salsa key/value.
|
||||
pub(crate) type SourceFileItemId = Id<SyntaxNode>;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct SourceItemId {
|
||||
file_id: FileId,
|
||||
/// None for the whole file.
|
||||
item_id: Option<SourceFileItemId>,
|
||||
}
|
||||
|
||||
/// Maps item's `SyntaxNode`s to `SourceFileItemId` and back.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct SourceFileItems {
|
||||
file_id: FileId,
|
||||
arena: Arena<SyntaxNode>,
|
||||
}
|
||||
|
||||
impl SourceFileItems {
|
||||
fn new(file_id: FileId) -> SourceFileItems {
|
||||
SourceFileItems {
|
||||
file_id,
|
||||
arena: Arena::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn alloc(&mut self, item: SyntaxNode) -> SourceFileItemId {
|
||||
self.arena.alloc(item)
|
||||
}
|
||||
pub fn id_of(&self, file_id: FileId, item: SyntaxNodeRef) -> SourceFileItemId {
|
||||
assert_eq!(
|
||||
self.file_id, file_id,
|
||||
"SourceFileItems: wrong file, expected {:?}, got {:?}",
|
||||
self.file_id, file_id
|
||||
);
|
||||
self.id_of_unchecked(item)
|
||||
}
|
||||
fn id_of_unchecked(&self, item: SyntaxNodeRef) -> SourceFileItemId {
|
||||
if let Some((id, _)) = self.arena.iter().find(|(_id, i)| i.borrowed() == item) {
|
||||
return id;
|
||||
}
|
||||
// This should not happen. Let's try to give a sensible diagnostics.
|
||||
if let Some((id, i)) = self.arena.iter().find(|(_id, i)| i.range() == item.range()) {
|
||||
// FIXME(#288): whyyy are we getting here?
|
||||
log::error!(
|
||||
"unequal syntax nodes with the same range:\n{:?}\n{:?}",
|
||||
item,
|
||||
i
|
||||
);
|
||||
return id;
|
||||
}
|
||||
panic!(
|
||||
"Can't find {:?} in SourceFileItems:\n{:?}",
|
||||
item,
|
||||
self.arena.iter().map(|(_id, i)| i).collect::<Vec<_>>(),
|
||||
);
|
||||
}
|
||||
pub fn id_of_source_file(&self) -> SourceFileItemId {
|
||||
let (id, _syntax) = self.arena.iter().next().unwrap();
|
||||
id
|
||||
}
|
||||
}
|
||||
|
||||
impl Index<SourceFileItemId> for SourceFileItems {
|
||||
type Output = SyntaxNode;
|
||||
fn index(&self, idx: SourceFileItemId) -> &SyntaxNode {
|
||||
&self.arena[idx]
|
||||
}
|
||||
}
|
||||
|
|
181
crates/ra_hir/src/macros.rs
Normal file
181
crates/ra_hir/src/macros.rs
Normal file
|
@ -0,0 +1,181 @@
|
|||
/// Machinery for macro expansion.
|
||||
///
|
||||
/// One of the more complicated things about macros is managing the source code
|
||||
/// that is produced after expansion. See `HirFileId` and `MacroCallId` for how
|
||||
/// do we do that.
|
||||
///
|
||||
/// When file-management question is resolved, all that is left is a token tree
|
||||
/// to token tree transformation plus hygent. We don't have either of thouse
|
||||
/// yet, so all macros are string based at the moment!
|
||||
use std::sync::Arc;
|
||||
|
||||
use ra_db::LocalSyntaxPtr;
|
||||
use ra_syntax::{
|
||||
TextRange, TextUnit, SourceFileNode, AstNode, SyntaxNode,
|
||||
ast::{self, NameOwner},
|
||||
};
|
||||
|
||||
use crate::{HirDatabase, MacroCallId};
|
||||
|
||||
// Hard-coded defs for now :-(
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum MacroDef {
|
||||
CTry,
|
||||
QueryGroup,
|
||||
}
|
||||
|
||||
impl MacroDef {
|
||||
/// Expands macro call, returning the expansion and offset to be used to
|
||||
/// convert ranges between expansion and original source.
|
||||
pub fn ast_expand(macro_call: ast::MacroCall) -> Option<(TextUnit, MacroExpansion)> {
|
||||
let (def, input) = MacroDef::from_call(macro_call)?;
|
||||
let exp = def.expand(input)?;
|
||||
let off = macro_call.token_tree()?.syntax().range().start();
|
||||
Some((off, exp))
|
||||
}
|
||||
|
||||
fn from_call(macro_call: ast::MacroCall) -> Option<(MacroDef, MacroInput)> {
|
||||
let def = {
|
||||
let path = macro_call.path()?;
|
||||
let name_ref = path.segment()?.name_ref()?;
|
||||
if name_ref.text() == "ctry" {
|
||||
MacroDef::CTry
|
||||
} else if name_ref.text() == "query_group" {
|
||||
MacroDef::QueryGroup
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let input = {
|
||||
let arg = macro_call.token_tree()?.syntax();
|
||||
MacroInput {
|
||||
text: arg.text().to_string(),
|
||||
}
|
||||
};
|
||||
Some((def, input))
|
||||
}
|
||||
|
||||
fn expand(self, input: MacroInput) -> Option<MacroExpansion> {
|
||||
match self {
|
||||
MacroDef::CTry => self.expand_ctry(input),
|
||||
MacroDef::QueryGroup => self.expand_query_group(input),
|
||||
}
|
||||
}
|
||||
fn expand_ctry(self, input: MacroInput) -> Option<MacroExpansion> {
|
||||
let text = format!(
|
||||
r"
|
||||
fn dummy() {{
|
||||
match {} {{
|
||||
None => return Ok(None),
|
||||
Some(it) => it,
|
||||
}}
|
||||
}}",
|
||||
input.text
|
||||
);
|
||||
let file = SourceFileNode::parse(&text);
|
||||
let match_expr = file.syntax().descendants().find_map(ast::MatchExpr::cast)?;
|
||||
let match_arg = match_expr.expr()?;
|
||||
let ptr = LocalSyntaxPtr::new(match_arg.syntax());
|
||||
let src_range = TextRange::offset_len(0.into(), TextUnit::of_str(&input.text));
|
||||
let ranges_map = vec![(src_range, match_arg.syntax().range())];
|
||||
let res = MacroExpansion {
|
||||
text,
|
||||
ranges_map,
|
||||
ptr,
|
||||
};
|
||||
Some(res)
|
||||
}
|
||||
fn expand_query_group(self, input: MacroInput) -> Option<MacroExpansion> {
|
||||
let anchor = "trait ";
|
||||
let pos = input.text.find(anchor)? + anchor.len();
|
||||
let trait_name = input.text[pos..]
|
||||
.chars()
|
||||
.take_while(|c| c.is_alphabetic())
|
||||
.collect::<String>();
|
||||
if trait_name.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let src_range = TextRange::offset_len((pos as u32).into(), TextUnit::of_str(&trait_name));
|
||||
let text = format!(r"trait {} {{ }}", trait_name);
|
||||
let file = SourceFileNode::parse(&text);
|
||||
let trait_def = file.syntax().descendants().find_map(ast::TraitDef::cast)?;
|
||||
let name = trait_def.name()?;
|
||||
let ptr = LocalSyntaxPtr::new(trait_def.syntax());
|
||||
let ranges_map = vec![(src_range, name.syntax().range())];
|
||||
let res = MacroExpansion {
|
||||
text,
|
||||
ranges_map,
|
||||
ptr,
|
||||
};
|
||||
Some(res)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct MacroInput {
|
||||
// Should be token trees
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct MacroExpansion {
|
||||
/// The result of macro expansion. Should be token tree as well.
|
||||
text: String,
|
||||
/// Correspondence between ranges in the original source code and ranges in
|
||||
/// the macro.
|
||||
ranges_map: Vec<(TextRange, TextRange)>,
|
||||
/// Implementation detail: internally, a macro is expanded to the whole file,
|
||||
/// even if it is an expression. This `ptr` selects the actual expansion from
|
||||
/// the expanded file.
|
||||
ptr: LocalSyntaxPtr,
|
||||
}
|
||||
|
||||
impl MacroExpansion {
|
||||
// FIXME: does not really make sense, macro expansion is not neccessary a
|
||||
// whole file. See `MacroExpansion::ptr` as well.
|
||||
pub(crate) fn file(&self) -> SourceFileNode {
|
||||
SourceFileNode::parse(&self.text)
|
||||
}
|
||||
|
||||
pub fn syntax(&self) -> SyntaxNode {
|
||||
self.ptr.resolve(&self.file())
|
||||
}
|
||||
/// Maps range in the source code to the range in the expanded code.
|
||||
pub fn map_range_forward(&self, src_range: TextRange) -> Option<TextRange> {
|
||||
for (s_range, t_range) in self.ranges_map.iter() {
|
||||
if src_range.is_subrange(&s_range) {
|
||||
let src_at_zero_range = src_range - src_range.start();
|
||||
let src_range_offset = src_range.start() - s_range.start();
|
||||
let src_range = src_at_zero_range + src_range_offset + t_range.start();
|
||||
return Some(src_range);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
/// Maps range in the expanded code to the range in the source code.
|
||||
pub fn map_range_back(&self, tgt_range: TextRange) -> Option<TextRange> {
|
||||
for (s_range, t_range) in self.ranges_map.iter() {
|
||||
if tgt_range.is_subrange(&t_range) {
|
||||
let tgt_at_zero_range = tgt_range - tgt_range.start();
|
||||
let tgt_range_offset = tgt_range.start() - t_range.start();
|
||||
let src_range = tgt_at_zero_range + tgt_range_offset + s_range.start();
|
||||
return Some(src_range);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn expand_macro_invocation(
|
||||
db: &impl HirDatabase,
|
||||
invoc: MacroCallId,
|
||||
) -> Option<Arc<MacroExpansion>> {
|
||||
let loc = invoc.loc(db);
|
||||
let syntax = db.file_item(loc.source_item_id);
|
||||
let syntax = syntax.borrowed();
|
||||
let macro_call = ast::MacroCall::cast(syntax).unwrap();
|
||||
|
||||
let (def, input) = MacroDef::from_call(macro_call)?;
|
||||
def.expand(input).map(Arc::new)
|
||||
}
|
|
@ -6,7 +6,7 @@ use ra_db::{LocationIntener, BaseDatabase, FilePosition, FileId, CrateGraph, Sou
|
|||
use relative_path::RelativePathBuf;
|
||||
use test_utils::{parse_fixture, CURSOR_MARKER, extract_offset};
|
||||
|
||||
use crate::{db, DefId, DefLoc};
|
||||
use crate::{db, DefId, DefLoc, MacroCallId, MacroCallLoc};
|
||||
|
||||
pub const WORKSPACE: SourceRootId = SourceRootId(0);
|
||||
|
||||
|
@ -95,6 +95,7 @@ impl MockDatabase {
|
|||
#[derive(Debug, Default)]
|
||||
struct IdMaps {
|
||||
defs: LocationIntener<DefLoc, DefId>,
|
||||
macros: LocationIntener<MacroCallLoc, MacroCallId>,
|
||||
}
|
||||
|
||||
impl salsa::Database for MockDatabase {
|
||||
|
@ -144,6 +145,11 @@ impl AsRef<LocationIntener<DefLoc, DefId>> for MockDatabase {
|
|||
&self.id_maps.defs
|
||||
}
|
||||
}
|
||||
impl AsRef<LocationIntener<MacroCallLoc, MacroCallId>> for MockDatabase {
|
||||
fn as_ref(&self) -> &LocationIntener<MacroCallLoc, MacroCallId> {
|
||||
&self.id_maps.macros
|
||||
}
|
||||
}
|
||||
|
||||
impl MockDatabase {
|
||||
pub(crate) fn log(&self, f: impl FnOnce()) -> Vec<salsa::Event<MockDatabase>> {
|
||||
|
@ -183,6 +189,8 @@ salsa::database_storage! {
|
|||
fn file_lines() for ra_db::FileLinesQuery;
|
||||
}
|
||||
impl db::HirDatabase {
|
||||
fn hir_source_file() for db::HirSourceFileQuery;
|
||||
fn expand_macro_invocation() for db::ExpandMacroCallQuery;
|
||||
fn module_tree() for db::ModuleTreeQuery;
|
||||
fn fn_scopes() for db::FnScopesQuery;
|
||||
fn file_items() for db::SourceFileItemsQuery;
|
||||
|
|
|
@ -15,6 +15,7 @@ use relative_path::RelativePathBuf;
|
|||
use crate::{
|
||||
Def, DefKind, DefLoc, DefId,
|
||||
Name, Path, PathKind, HirDatabase, SourceItemId, SourceFileItemId, Crate,
|
||||
HirFileId,
|
||||
arena::{Arena, Id},
|
||||
};
|
||||
|
||||
|
@ -48,13 +49,17 @@ impl Module {
|
|||
/// Returns `None` for the root module
|
||||
pub fn parent_link_source(&self, db: &impl HirDatabase) -> Option<(FileId, ast::ModuleNode)> {
|
||||
let link = self.module_id.parent_link(&self.tree)?;
|
||||
let file_id = link.owner(&self.tree).source(&self.tree).file_id();
|
||||
let file_id = link
|
||||
.owner(&self.tree)
|
||||
.source(&self.tree)
|
||||
.file_id()
|
||||
.as_original_file();
|
||||
let src = link.bind_source(&self.tree, db);
|
||||
Some((file_id, src))
|
||||
}
|
||||
|
||||
pub fn source(&self) -> ModuleSource {
|
||||
self.module_id.source(&self.tree)
|
||||
pub fn file_id(&self) -> FileId {
|
||||
self.source().file_id().as_original_file()
|
||||
}
|
||||
|
||||
/// Parent module. Returns `None` if this is a root module.
|
||||
|
@ -69,7 +74,7 @@ impl Module {
|
|||
/// Returns the crate this module is part of.
|
||||
pub fn krate(&self, db: &impl HirDatabase) -> Option<Crate> {
|
||||
let root_id = self.module_id.crate_root(&self.tree);
|
||||
let file_id = root_id.source(&self.tree).file_id();
|
||||
let file_id = root_id.source(&self.tree).file_id().as_original_file();
|
||||
let crate_graph = db.crate_graph();
|
||||
let crate_id = crate_graph.crate_id_for_crate_root(file_id)?;
|
||||
Some(Crate::new(crate_id))
|
||||
|
@ -162,6 +167,10 @@ impl Module {
|
|||
pub fn problems(&self, db: &impl HirDatabase) -> Vec<(SyntaxNode, Problem)> {
|
||||
self.module_id.problems(&self.tree, db)
|
||||
}
|
||||
|
||||
pub(crate) fn source(&self) -> ModuleSource {
|
||||
self.module_id.source(&self.tree)
|
||||
}
|
||||
}
|
||||
|
||||
/// Physically, rust source is organized as a set of files, but logically it is
|
||||
|
@ -291,18 +300,18 @@ pub struct ModuleData {
|
|||
|
||||
impl ModuleSource {
|
||||
// precondition: item_id **must** point to module
|
||||
fn new(file_id: FileId, item_id: Option<SourceFileItemId>) -> ModuleSource {
|
||||
fn new(file_id: HirFileId, item_id: Option<SourceFileItemId>) -> ModuleSource {
|
||||
let source_item_id = SourceItemId { file_id, item_id };
|
||||
ModuleSource(source_item_id)
|
||||
}
|
||||
|
||||
pub(crate) fn new_file(file_id: FileId) -> ModuleSource {
|
||||
pub(crate) fn new_file(file_id: HirFileId) -> ModuleSource {
|
||||
ModuleSource::new(file_id, None)
|
||||
}
|
||||
|
||||
pub(crate) fn new_inline(
|
||||
db: &impl HirDatabase,
|
||||
file_id: FileId,
|
||||
file_id: HirFileId,
|
||||
m: ast::Module,
|
||||
) -> ModuleSource {
|
||||
assert!(!m.has_semi());
|
||||
|
@ -311,7 +320,7 @@ impl ModuleSource {
|
|||
ModuleSource::new(file_id, Some(item_id))
|
||||
}
|
||||
|
||||
pub fn file_id(self) -> FileId {
|
||||
pub(crate) fn file_id(self) -> HirFileId {
|
||||
self.0.file_id
|
||||
}
|
||||
|
||||
|
|
|
@ -64,7 +64,7 @@ fn create_module_tree<'a>(
|
|||
|
||||
let source_root = db.source_root(source_root);
|
||||
for &file_id in source_root.files.values() {
|
||||
let source = ModuleSource::new_file(file_id);
|
||||
let source = ModuleSource::new_file(file_id.into());
|
||||
if visited.contains(&source) {
|
||||
continue; // TODO: use explicit crate_roots here
|
||||
}
|
||||
|
@ -123,7 +123,7 @@ fn build_subtree(
|
|||
visited,
|
||||
roots,
|
||||
Some(link),
|
||||
ModuleSource::new_file(file_id),
|
||||
ModuleSource::new_file(file_id.into()),
|
||||
),
|
||||
})
|
||||
.collect::<Cancelable<Vec<_>>>()?;
|
||||
|
@ -155,7 +155,7 @@ fn resolve_submodule(
|
|||
name: &Name,
|
||||
) -> (Vec<FileId>, Option<Problem>) {
|
||||
// FIXME: handle submodules of inline modules properly
|
||||
let file_id = source.file_id();
|
||||
let file_id = source.file_id().original_file(db);
|
||||
let source_root_id = db.file_source_root(file_id);
|
||||
let path = db.file_relative_path(file_id);
|
||||
let root = RelativePathBuf::default();
|
||||
|
|
|
@ -22,10 +22,10 @@ use ra_syntax::{
|
|||
SyntaxKind::{self, *},
|
||||
ast::{self, AstNode}
|
||||
};
|
||||
use ra_db::SourceRootId;
|
||||
use ra_db::{SourceRootId, Cancelable, FileId};
|
||||
|
||||
use crate::{
|
||||
Cancelable, FileId,
|
||||
HirFileId,
|
||||
DefId, DefLoc, DefKind,
|
||||
SourceItemId, SourceFileItemId, SourceFileItems,
|
||||
Path, PathKind,
|
||||
|
@ -70,7 +70,7 @@ pub struct InputModuleItems {
|
|||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
struct ModuleItem {
|
||||
id: SourceFileItemId,
|
||||
id: SourceItemId,
|
||||
name: Name,
|
||||
kind: SyntaxKind,
|
||||
vis: Vis,
|
||||
|
@ -95,9 +95,11 @@ pub struct NamedImport {
|
|||
}
|
||||
|
||||
impl NamedImport {
|
||||
// FIXME: this is only here for one use-case in completion. Seems like a
|
||||
// pretty gross special case.
|
||||
pub fn range(&self, db: &impl HirDatabase, file_id: FileId) -> TextRange {
|
||||
let source_item_id = SourceItemId {
|
||||
file_id,
|
||||
file_id: file_id.into(),
|
||||
item_id: Some(self.file_item_id),
|
||||
};
|
||||
let syntax = db.file_item(source_item_id);
|
||||
|
@ -209,24 +211,28 @@ impl<T> PerNs<T> {
|
|||
}
|
||||
|
||||
impl InputModuleItems {
|
||||
pub(crate) fn new<'a>(
|
||||
pub(crate) fn add_item(
|
||||
&mut self,
|
||||
file_id: HirFileId,
|
||||
file_items: &SourceFileItems,
|
||||
items: impl Iterator<Item = ast::ModuleItem<'a>>,
|
||||
) -> InputModuleItems {
|
||||
let mut res = InputModuleItems::default();
|
||||
for item in items {
|
||||
res.add_item(file_items, item);
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
fn add_item(&mut self, file_items: &SourceFileItems, item: ast::ModuleItem) -> Option<()> {
|
||||
item: ast::ModuleItem,
|
||||
) -> Option<()> {
|
||||
match item {
|
||||
ast::ModuleItem::StructDef(it) => self.items.push(ModuleItem::new(file_items, it)?),
|
||||
ast::ModuleItem::EnumDef(it) => self.items.push(ModuleItem::new(file_items, it)?),
|
||||
ast::ModuleItem::FnDef(it) => self.items.push(ModuleItem::new(file_items, it)?),
|
||||
ast::ModuleItem::TraitDef(it) => self.items.push(ModuleItem::new(file_items, it)?),
|
||||
ast::ModuleItem::TypeDef(it) => self.items.push(ModuleItem::new(file_items, it)?),
|
||||
ast::ModuleItem::StructDef(it) => {
|
||||
self.items.push(ModuleItem::new(file_id, file_items, it)?)
|
||||
}
|
||||
ast::ModuleItem::EnumDef(it) => {
|
||||
self.items.push(ModuleItem::new(file_id, file_items, it)?)
|
||||
}
|
||||
ast::ModuleItem::FnDef(it) => {
|
||||
self.items.push(ModuleItem::new(file_id, file_items, it)?)
|
||||
}
|
||||
ast::ModuleItem::TraitDef(it) => {
|
||||
self.items.push(ModuleItem::new(file_id, file_items, it)?)
|
||||
}
|
||||
ast::ModuleItem::TypeDef(it) => {
|
||||
self.items.push(ModuleItem::new(file_id, file_items, it)?)
|
||||
}
|
||||
ast::ModuleItem::ImplItem(_) => {
|
||||
// impls don't define items
|
||||
}
|
||||
|
@ -234,9 +240,15 @@ impl InputModuleItems {
|
|||
ast::ModuleItem::ExternCrateItem(_) => {
|
||||
// TODO
|
||||
}
|
||||
ast::ModuleItem::ConstDef(it) => self.items.push(ModuleItem::new(file_items, it)?),
|
||||
ast::ModuleItem::StaticDef(it) => self.items.push(ModuleItem::new(file_items, it)?),
|
||||
ast::ModuleItem::Module(it) => self.items.push(ModuleItem::new(file_items, it)?),
|
||||
ast::ModuleItem::ConstDef(it) => {
|
||||
self.items.push(ModuleItem::new(file_id, file_items, it)?)
|
||||
}
|
||||
ast::ModuleItem::StaticDef(it) => {
|
||||
self.items.push(ModuleItem::new(file_id, file_items, it)?)
|
||||
}
|
||||
ast::ModuleItem::Module(it) => {
|
||||
self.items.push(ModuleItem::new(file_id, file_items, it)?)
|
||||
}
|
||||
}
|
||||
Some(())
|
||||
}
|
||||
|
@ -258,11 +270,16 @@ impl InputModuleItems {
|
|||
}
|
||||
|
||||
impl ModuleItem {
|
||||
fn new<'a>(file_items: &SourceFileItems, item: impl ast::NameOwner<'a>) -> Option<ModuleItem> {
|
||||
fn new<'a>(
|
||||
file_id: HirFileId,
|
||||
file_items: &SourceFileItems,
|
||||
item: impl ast::NameOwner<'a>,
|
||||
) -> Option<ModuleItem> {
|
||||
let name = item.name()?.as_name();
|
||||
let kind = item.syntax().kind();
|
||||
let vis = Vis::Other;
|
||||
let id = file_items.id_of_unchecked(item.syntax());
|
||||
let item_id = Some(file_items.id_of_unchecked(item.syntax()));
|
||||
let id = SourceItemId { file_id, item_id };
|
||||
let res = ModuleItem {
|
||||
id,
|
||||
name,
|
||||
|
@ -302,7 +319,7 @@ where
|
|||
|
||||
pub(crate) fn resolve(mut self) -> Cancelable<ItemMap> {
|
||||
for (&module_id, items) in self.input.iter() {
|
||||
self.populate_module(module_id, items)?;
|
||||
self.populate_module(module_id, Arc::clone(items))?;
|
||||
}
|
||||
|
||||
for &module_id in self.input.keys() {
|
||||
|
@ -312,9 +329,11 @@ where
|
|||
Ok(self.result)
|
||||
}
|
||||
|
||||
fn populate_module(&mut self, module_id: ModuleId, input: &InputModuleItems) -> Cancelable<()> {
|
||||
let file_id = module_id.source(&self.module_tree).file_id();
|
||||
|
||||
fn populate_module(
|
||||
&mut self,
|
||||
module_id: ModuleId,
|
||||
input: Arc<InputModuleItems>,
|
||||
) -> Cancelable<()> {
|
||||
let mut module_items = ModuleScope::default();
|
||||
|
||||
// Populate extern crates prelude
|
||||
|
@ -322,7 +341,8 @@ where
|
|||
let root_id = module_id.crate_root(&self.module_tree);
|
||||
let file_id = root_id.source(&self.module_tree).file_id();
|
||||
let crate_graph = self.db.crate_graph();
|
||||
if let Some(crate_id) = crate_graph.crate_id_for_crate_root(file_id) {
|
||||
if let Some(crate_id) = crate_graph.crate_id_for_crate_root(file_id.as_original_file())
|
||||
{
|
||||
let krate = Crate::new(crate_id);
|
||||
for dep in krate.dependencies(self.db) {
|
||||
if let Some(module) = dep.krate.root_module(self.db)? {
|
||||
|
@ -362,10 +382,7 @@ where
|
|||
kind: k,
|
||||
source_root_id: self.source_root,
|
||||
module_id,
|
||||
source_item_id: SourceItemId {
|
||||
file_id,
|
||||
item_id: Some(item.id),
|
||||
},
|
||||
source_item_id: item.id,
|
||||
};
|
||||
def_loc.id(self.db)
|
||||
});
|
||||
|
|
|
@ -77,6 +77,35 @@ fn item_map_smoke_test() {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn item_map_contains_items_from_expansions() {
|
||||
let (item_map, module_id) = item_map(
|
||||
"
|
||||
//- /lib.rs
|
||||
mod foo;
|
||||
|
||||
use crate::foo::bar::Baz;
|
||||
<|>
|
||||
|
||||
//- /foo/mod.rs
|
||||
pub mod bar;
|
||||
|
||||
//- /foo/bar.rs
|
||||
salsa::query_group! {
|
||||
trait Baz {}
|
||||
}
|
||||
",
|
||||
);
|
||||
check_module_item_map(
|
||||
&item_map,
|
||||
module_id,
|
||||
"
|
||||
Baz: t
|
||||
foo: t
|
||||
",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn item_map_using_self() {
|
||||
let (item_map, module_id) = item_map(
|
||||
|
@ -141,6 +170,59 @@ fn item_map_across_crates() {
|
|||
|
||||
#[test]
|
||||
fn typing_inside_a_function_should_not_invalidate_item_map() {
|
||||
let (mut db, pos) = MockDatabase::with_position(
|
||||
"
|
||||
//- /lib.rs
|
||||
mod foo;
|
||||
|
||||
use crate::foo::bar::Baz;
|
||||
|
||||
//- /foo/mod.rs
|
||||
pub mod bar;
|
||||
|
||||
//- /foo/bar.rs
|
||||
<|>
|
||||
salsa::query_group! {
|
||||
trait Baz {
|
||||
fn foo() -> i32 { 1 + 1 }
|
||||
}
|
||||
}
|
||||
",
|
||||
);
|
||||
let source_root = db.file_source_root(pos.file_id);
|
||||
{
|
||||
let events = db.log_executed(|| {
|
||||
db.item_map(source_root).unwrap();
|
||||
});
|
||||
assert!(format!("{:?}", events).contains("item_map"))
|
||||
}
|
||||
|
||||
let new_text = "
|
||||
salsa::query_group! {
|
||||
trait Baz {
|
||||
fn foo() -> i32 { 92 }
|
||||
}
|
||||
}
|
||||
"
|
||||
.to_string();
|
||||
|
||||
db.query_mut(ra_db::FileTextQuery)
|
||||
.set(pos.file_id, Arc::new(new_text));
|
||||
|
||||
{
|
||||
let events = db.log_executed(|| {
|
||||
db.item_map(source_root).unwrap();
|
||||
});
|
||||
assert!(
|
||||
!format!("{:?}", events).contains("item_map"),
|
||||
"{:#?}",
|
||||
events
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typing_inside_a_function_inside_a_macro_should_not_invalidate_item_map() {
|
||||
let (mut db, pos) = MockDatabase::with_position(
|
||||
"
|
||||
//- /lib.rs
|
||||
|
@ -183,7 +265,7 @@ fn typing_inside_a_function_should_not_invalidate_item_map() {
|
|||
db.item_map(source_root).unwrap();
|
||||
});
|
||||
assert!(
|
||||
!format!("{:?}", events).contains("_item_map"),
|
||||
!format!("{:?}", events).contains("item_map"),
|
||||
"{:#?}",
|
||||
events
|
||||
)
|
||||
|
|
|
@ -8,10 +8,11 @@ use ra_syntax::{
|
|||
AstNode, SyntaxNode,
|
||||
ast::{self, NameOwner, ModuleItemOwner}
|
||||
};
|
||||
use ra_db::{SourceRootId, FileId, Cancelable,};
|
||||
use ra_db::{SourceRootId, Cancelable,};
|
||||
|
||||
use crate::{
|
||||
SourceFileItems, SourceItemId, DefKind, Function, DefId, Name, AsName,
|
||||
SourceFileItems, SourceItemId, DefKind, Function, DefId, Name, AsName, HirFileId,
|
||||
MacroCallLoc,
|
||||
db::HirDatabase,
|
||||
function::FnScopes,
|
||||
module::{
|
||||
|
@ -47,25 +48,17 @@ pub(super) fn enum_data(db: &impl HirDatabase, def_id: DefId) -> Cancelable<Arc<
|
|||
Ok(Arc::new(EnumData::new(enum_def.borrowed())))
|
||||
}
|
||||
|
||||
pub(super) fn file_items(db: &impl HirDatabase, file_id: FileId) -> Arc<SourceFileItems> {
|
||||
let mut res = SourceFileItems::new(file_id);
|
||||
let source_file = db.source_file(file_id);
|
||||
pub(super) fn file_items(db: &impl HirDatabase, file_id: HirFileId) -> Arc<SourceFileItems> {
|
||||
let source_file = db.hir_source_file(file_id);
|
||||
let source_file = source_file.borrowed();
|
||||
source_file
|
||||
.syntax()
|
||||
.descendants()
|
||||
.filter_map(ast::ModuleItem::cast)
|
||||
.map(|it| it.syntax().owned())
|
||||
.for_each(|it| {
|
||||
res.alloc(it);
|
||||
});
|
||||
let res = SourceFileItems::new(file_id, source_file);
|
||||
Arc::new(res)
|
||||
}
|
||||
|
||||
pub(super) fn file_item(db: &impl HirDatabase, source_item_id: SourceItemId) -> SyntaxNode {
|
||||
match source_item_id.item_id {
|
||||
Some(id) => db.file_items(source_item_id.file_id)[id].clone(),
|
||||
None => db.source_file(source_item_id.file_id).syntax().owned(),
|
||||
None => db.hir_source_file(source_item_id.file_id).syntax().owned(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -87,7 +80,7 @@ pub(crate) fn submodules(
|
|||
|
||||
fn collect_submodules<'a>(
|
||||
db: &impl HirDatabase,
|
||||
file_id: FileId,
|
||||
file_id: HirFileId,
|
||||
root: impl ast::ModuleItemOwner<'a>,
|
||||
) -> Vec<Submodule> {
|
||||
modules(root)
|
||||
|
@ -119,24 +112,48 @@ pub(crate) fn modules<'a>(
|
|||
|
||||
pub(super) fn input_module_items(
|
||||
db: &impl HirDatabase,
|
||||
source_root: SourceRootId,
|
||||
source_root_id: SourceRootId,
|
||||
module_id: ModuleId,
|
||||
) -> Cancelable<Arc<InputModuleItems>> {
|
||||
let module_tree = db.module_tree(source_root)?;
|
||||
let module_tree = db.module_tree(source_root_id)?;
|
||||
let source = module_id.source(&module_tree);
|
||||
let file_items = db.file_items(source.file_id());
|
||||
let res = match source.resolve(db) {
|
||||
ModuleSourceNode::SourceFile(it) => {
|
||||
let items = it.borrowed().items();
|
||||
InputModuleItems::new(&file_items, items)
|
||||
let file_id = source.file_id();
|
||||
let file_items = db.file_items(file_id);
|
||||
let fill = |acc: &mut InputModuleItems, items: &mut Iterator<Item = ast::ItemOrMacro>| {
|
||||
for item in items {
|
||||
match item {
|
||||
ast::ItemOrMacro::Item(it) => {
|
||||
acc.add_item(file_id, &file_items, it);
|
||||
}
|
||||
ast::ItemOrMacro::Macro(macro_call) => {
|
||||
let item_id = file_items.id_of_unchecked(macro_call.syntax());
|
||||
let loc = MacroCallLoc {
|
||||
source_root_id,
|
||||
module_id,
|
||||
source_item_id: SourceItemId {
|
||||
file_id,
|
||||
item_id: Some(item_id),
|
||||
},
|
||||
};
|
||||
let id = loc.id(db);
|
||||
let file_id = HirFileId::from(id);
|
||||
let file_items = db.file_items(file_id);
|
||||
//FIXME: expand recursively
|
||||
for item in db.hir_source_file(file_id).borrowed().items() {
|
||||
acc.add_item(file_id, &file_items, item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let mut res = InputModuleItems::default();
|
||||
match source.resolve(db) {
|
||||
ModuleSourceNode::SourceFile(it) => fill(&mut res, &mut it.borrowed().items_with_macros()),
|
||||
ModuleSourceNode::Module(it) => {
|
||||
let items = it
|
||||
.borrowed()
|
||||
.item_list()
|
||||
.into_iter()
|
||||
.flat_map(|it| it.items());
|
||||
InputModuleItems::new(&file_items, items)
|
||||
if let Some(item_list) = it.borrowed().item_list() {
|
||||
fill(&mut res, &mut item_list.items_with_macros())
|
||||
}
|
||||
}
|
||||
};
|
||||
Ok(Arc::new(res))
|
||||
|
|
|
@ -20,7 +20,7 @@ use crate::{
|
|||
|
||||
/// Locates the module by `FileId`. Picks topmost module in the file.
|
||||
pub fn module_from_file_id(db: &impl HirDatabase, file_id: FileId) -> Cancelable<Option<Module>> {
|
||||
let module_source = ModuleSource::new_file(file_id);
|
||||
let module_source = ModuleSource::new_file(file_id.into());
|
||||
module_from_source(db, module_source)
|
||||
}
|
||||
|
||||
|
@ -50,8 +50,8 @@ pub fn module_from_position(
|
|||
) -> Cancelable<Option<Module>> {
|
||||
let file = db.source_file(position.file_id);
|
||||
let module_source = match find_node_at_offset::<ast::Module>(file.syntax(), position.offset) {
|
||||
Some(m) if !m.has_semi() => ModuleSource::new_inline(db, position.file_id, m),
|
||||
_ => ModuleSource::new_file(position.file_id),
|
||||
Some(m) if !m.has_semi() => ModuleSource::new_inline(db, position.file_id.into(), m),
|
||||
_ => ModuleSource::new_file(position.file_id.into()),
|
||||
};
|
||||
module_from_source(db, module_source)
|
||||
}
|
||||
|
@ -67,9 +67,9 @@ pub fn module_from_child_node(
|
|||
.filter_map(ast::Module::cast)
|
||||
.find(|it| !it.has_semi())
|
||||
{
|
||||
ModuleSource::new_inline(db, file_id, m)
|
||||
ModuleSource::new_inline(db, file_id.into(), m)
|
||||
} else {
|
||||
ModuleSource::new_file(file_id)
|
||||
ModuleSource::new_file(file_id.into())
|
||||
};
|
||||
module_from_source(db, module_source)
|
||||
}
|
||||
|
@ -78,7 +78,7 @@ fn module_from_source(
|
|||
db: &impl HirDatabase,
|
||||
module_source: ModuleSource,
|
||||
) -> Cancelable<Option<Module>> {
|
||||
let source_root_id = db.file_source_root(module_source.file_id());
|
||||
let source_root_id = db.file_source_root(module_source.file_id().as_original_file());
|
||||
let module_tree = db.module_tree(source_root_id)?;
|
||||
let m = module_tree
|
||||
.modules_with_sources()
|
||||
|
|
|
@ -48,10 +48,40 @@ pub trait FnDefOwner<'a>: AstNode<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
// ModuleItem
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ItemOrMacro<'a> {
|
||||
Item(ModuleItem<'a>),
|
||||
Macro(MacroCall<'a>),
|
||||
}
|
||||
|
||||
impl<'a> AstNode<'a> for ItemOrMacro<'a> {
|
||||
fn cast(syntax: SyntaxNodeRef<'a>) -> Option<Self> {
|
||||
let res = if let Some(item) = ModuleItem::cast(syntax) {
|
||||
ItemOrMacro::Item(item)
|
||||
} else if let Some(macro_call) = MacroCall::cast(syntax) {
|
||||
ItemOrMacro::Macro(macro_call)
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
Some(res)
|
||||
}
|
||||
fn syntax(self) -> SyntaxNodeRef<'a> {
|
||||
match self {
|
||||
ItemOrMacro::Item(it) => it.syntax(),
|
||||
ItemOrMacro::Macro(it) => it.syntax(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ModuleItemOwner<'a>: AstNode<'a> {
|
||||
fn items(self) -> AstChildren<'a, ModuleItem<'a>> {
|
||||
children(self)
|
||||
}
|
||||
|
||||
fn items_with_macros(self) -> AstChildren<'a, ItemOrMacro<'a>> {
|
||||
children(self)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait TypeParamsOwner<'a>: AstNode<'a> {
|
||||
|
|
|
@ -51,7 +51,7 @@ use ra_text_edit::AtomTextEdit;
|
|||
use crate::yellow::GreenNode;
|
||||
|
||||
/// `SourceFileNode` represents a parse tree for a single Rust file.
|
||||
pub use crate::ast::SourceFileNode;
|
||||
pub use crate::ast::{SourceFile, SourceFileNode};
|
||||
|
||||
impl SourceFileNode {
|
||||
fn new(green: GreenNode, errors: Vec<SyntaxError>) -> SourceFileNode {
|
||||
|
|
Loading…
Reference in a new issue