use std::collections::HashMap;
use mago_syntax::ast::*;
use crate::Backend;
use crate::util::short_name;
impl Backend {
pub(crate) fn extract_use_statements_from_statements<'a>(
statements: impl Iterator<Item = &'a Statement<'a>>,
use_map: &mut HashMap<String, String>,
) {
for statement in statements {
match statement {
Statement::Use(use_stmt) => {
Self::extract_use_items(&use_stmt.items, use_map);
}
Statement::Namespace(namespace) => {
Self::extract_use_statements_from_statements(
namespace.statements().iter(),
use_map,
);
}
_ => {}
}
}
}
pub(crate) fn extract_use_items(items: &UseItems, use_map: &mut HashMap<String, String>) {
match items {
UseItems::Sequence(seq) => {
for item in seq.items.iter() {
Self::register_use_item(item, None, use_map);
}
}
UseItems::TypedSequence(seq) => {
for item in seq.items.iter() {
Self::register_use_item(item, None, use_map);
}
}
UseItems::TypedList(list) => {
let prefix = list.namespace.value();
for item in list.items.iter() {
Self::register_use_item(item, Some(prefix), use_map);
}
}
UseItems::MixedList(list) => {
let prefix = list.namespace.value();
for maybe_typed in list.items.iter() {
Self::register_use_item(&maybe_typed.item, Some(prefix), use_map);
}
}
}
}
fn register_use_item(
item: &UseItem,
group_prefix: Option<&str>,
use_map: &mut HashMap<String, String>,
) {
let item_name = item.name.value();
let fqn = if let Some(prefix) = group_prefix {
format!("{}\\{}", prefix, item_name)
} else {
item_name.to_string()
};
let alias_name = if let Some(ref alias) = item.alias {
alias.identifier.value.to_string()
} else {
short_name(&fqn).to_string()
};
use_map.insert(alias_name, fqn);
}
pub(crate) fn extract_namespace_from_statements<'a>(
statements: impl Iterator<Item = &'a Statement<'a>>,
) -> Option<String> {
for statement in statements {
if let Statement::Namespace(namespace) = statement {
if let Some(ident) = &namespace.name {
let name = ident.value();
if !name.is_empty() {
return Some(name.to_string());
}
}
}
}
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn grouped_use_populates_use_map_and_resolved_names() {
let backend = Backend::new_test();
let uri = "file:///test.php";
let content = r#"<?php
namespace Controllers\Registration;
use Models\Common\{Disciplines, TeamMembers, TournamentLeagueRosters, TournamentsLeagues};
class RegistrationController {
public function foo(Disciplines $d): TeamMembers {
}
}
"#;
backend.update_ast(uri, content);
let use_map = backend.use_map.read();
let file_map = use_map
.get(uri)
.expect("use_map should have an entry for the file");
assert_eq!(
file_map.get("Disciplines"),
Some(&"Models\\Common\\Disciplines".to_string()),
"Disciplines should be in the use_map"
);
assert_eq!(
file_map.get("TeamMembers"),
Some(&"Models\\Common\\TeamMembers".to_string()),
"TeamMembers should be in the use_map"
);
assert_eq!(
file_map.get("TournamentLeagueRosters"),
Some(&"Models\\Common\\TournamentLeagueRosters".to_string()),
"TournamentLeagueRosters should be in the use_map"
);
assert_eq!(
file_map.get("TournamentsLeagues"),
Some(&"Models\\Common\\TournamentsLeagues".to_string()),
"TournamentsLeagues should be in the use_map"
);
drop(use_map);
let resolved = backend.resolved_names.read();
let rn = resolved
.get(uri)
.expect("resolved_names should have an entry for the file");
let hint_offset = content
.find("Disciplines $d")
.expect("should find Disciplines type hint") as u32;
assert_eq!(
rn.get(hint_offset),
Some("Models\\Common\\Disciplines"),
"mago-names should resolve Disciplines type hint to FQN"
);
let ret_offset = content
.find("): TeamMembers")
.map(|p| p + "): ".len())
.expect("should find TeamMembers return type") as u32;
assert_eq!(
rn.get(ret_offset),
Some("Models\\Common\\TeamMembers"),
"mago-names should resolve TeamMembers return type to FQN"
);
}
#[test]
fn grouped_use_with_alias() {
let backend = Backend::new_test();
let uri = "file:///test.php";
let content = "<?php\nuse Models\\Common\\{Disciplines as Disc, TeamMembers};\n\nclass X extends Disc {}\n";
backend.update_ast(uri, content);
let use_map = backend.use_map.read();
let file_map = use_map.get(uri).expect("use_map entry");
assert_eq!(
file_map.get("Disc"),
Some(&"Models\\Common\\Disciplines".to_string()),
"aliased short name should map to the full FQN"
);
assert_eq!(
file_map.get("TeamMembers"),
Some(&"Models\\Common\\TeamMembers".to_string()),
);
assert!(
!file_map.contains_key("Disciplines"),
"original name should not be in the use_map when aliased"
);
}
#[test]
fn use_function_populates_use_map() {
let backend = Backend::new_test();
let uri = "file:///test.php";
let content = r#"<?php
namespace Tests\Unit;
use function PHPUnit\Framework\assertSame;
use function PHPUnit\Framework\assertCount;
class MyTest {}
"#;
backend.update_ast(uri, content);
let use_map = backend.use_map.read();
let file_map = use_map
.get(uri)
.expect("use_map should have an entry for the file");
assert_eq!(
file_map.get("assertSame"),
Some(&"PHPUnit\\Framework\\assertSame".to_string()),
"use function should add assertSame to use_map"
);
assert_eq!(
file_map.get("assertCount"),
Some(&"PHPUnit\\Framework\\assertCount".to_string()),
"use function should add assertCount to use_map"
);
}
#[test]
fn use_function_grouped_populates_use_map() {
let backend = Backend::new_test();
let uri = "file:///test.php";
let content = r#"<?php
namespace Tests\Unit;
use function PHPUnit\Framework\{assertSame, assertCount};
class MyTest {}
"#;
backend.update_ast(uri, content);
let use_map = backend.use_map.read();
let file_map = use_map
.get(uri)
.expect("use_map should have an entry for the file");
assert_eq!(
file_map.get("assertSame"),
Some(&"PHPUnit\\Framework\\assertSame".to_string()),
"grouped use function should add assertSame to use_map"
);
assert_eq!(
file_map.get("assertCount"),
Some(&"PHPUnit\\Framework\\assertCount".to_string()),
"grouped use function should add assertCount to use_map"
);
}
#[test]
fn use_const_populates_use_map() {
let backend = Backend::new_test();
let uri = "file:///test.php";
let content = r#"<?php
namespace App;
use const JSON_THROW_ON_ERROR;
class MyClass {}
"#;
backend.update_ast(uri, content);
let use_map = backend.use_map.read();
let file_map = use_map
.get(uri)
.expect("use_map should have an entry for the file");
assert_eq!(
file_map.get("JSON_THROW_ON_ERROR"),
Some(&"JSON_THROW_ON_ERROR".to_string()),
"use const should add JSON_THROW_ON_ERROR to use_map"
);
}
#[test]
fn mixed_use_includes_functions_and_consts() {
let backend = Backend::new_test();
let uri = "file:///test.php";
let content = "<?php\nuse App\\{MyClass, function myFunc, const MY_CONST};\n";
backend.update_ast(uri, content);
let use_map = backend.use_map.read();
let file_map = use_map
.get(uri)
.expect("use_map should have an entry for the file");
assert_eq!(
file_map.get("MyClass"),
Some(&"App\\MyClass".to_string()),
"mixed use should include class import"
);
assert_eq!(
file_map.get("myFunc"),
Some(&"App\\myFunc".to_string()),
"mixed use should include function import"
);
assert_eq!(
file_map.get("MY_CONST"),
Some(&"App\\MY_CONST".to_string()),
"mixed use should include const import"
);
}
}