Add testing infrastructure for type inference

- move dir_tests to test_utils for that.
This commit is contained in:
Florian Diebold 2018-12-23 12:05:54 +01:00
parent 3899898d75
commit 7348f7883f
9 changed files with 223 additions and 125 deletions

View file

@ -5,7 +5,8 @@ use std::{
use ra_editor::{self, find_node_at_offset, FileSymbol, LineIndex, LocalEdit};
use ra_syntax::{
ast::{self, ArgListOwner, Expr, NameOwner},
ast::{self, ArgListOwner, Expr, NameOwner, FnDef},
algo::find_covering_node,
AstNode, SourceFileNode,
SyntaxKind::*,
SyntaxNodeRef, TextRange, TextUnit,
@ -510,6 +511,17 @@ impl AnalysisImpl {
Ok(None)
}
pub fn type_of(&self, file_id: FileId, range: TextRange) -> Cancelable<Option<String>> {
let file = self.db.source_file(file_id);
let syntax = file.syntax();
let node = find_covering_node(syntax, range);
let parent_fn = node.ancestors().filter_map(FnDef::cast).next();
let parent_fn = if let Some(p) = parent_fn { p } else { return Ok(None) };
let function = ctry!(source_binder::function_from_source(&*self.db, file_id, parent_fn)?);
let infer = function.infer(&*self.db);
Ok(infer.type_of_node(node).map(|t| t.to_string()))
}
fn index_resolve(&self, name_ref: ast::NameRef) -> Cancelable<Vec<(FileId, FileSymbol)>> {
let name = name_ref.text();
let mut query = Query::new(name.to_string());

View file

@ -366,6 +366,9 @@ impl Analysis {
) -> Cancelable<Option<(FnSignatureInfo, Option<usize>)>> {
self.imp.resolve_callable(position)
}
pub fn type_of(&self, file_id: FileId, range: TextRange) -> Cancelable<Option<String>> {
self.imp.type_of(file_id, range)
}
}
pub struct LibraryData {

View file

@ -24,6 +24,15 @@ impl MockDatabase {
(db, source_root)
}
pub(crate) fn with_single_file(text: &str) -> (MockDatabase, SourceRoot, FileId) {
let mut db = MockDatabase::default();
let mut source_root = SourceRoot::default();
let file_id = db.add_file(&mut source_root, "/main.rs", text);
db.query_mut(ra_db::SourceRootQuery)
.set(WORKSPACE, Arc::new(source_root.clone()));
(db, source_root, file_id)
}
pub(crate) fn with_position(fixture: &str) -> (MockDatabase, FilePosition) {
let (db, _, position) = MockDatabase::from_fixture(fixture);
let position = position.expect("expected a marker ( <|> )");

View file

@ -6,6 +6,7 @@ use rustc_hash::{FxHashMap, FxHashSet};
use std::sync::Arc;
use std::collections::HashMap;
use std::fmt;
use ra_db::LocalSyntaxPtr;
use ra_syntax::{
@ -184,11 +185,40 @@ impl Ty {
}
}
impl fmt::Display for Ty {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Ty::Bool => write!(f, "bool"),
Ty::Char => write!(f, "char"),
Ty::Int(t) => write!(f, "{}", t.ty_to_string()),
Ty::Uint(t) => write!(f, "{}", t.ty_to_string()),
Ty::Float(t) => write!(f, "{}", t.ty_to_string()),
Ty::Str => write!(f, "str"),
Ty::Slice(t) => write!(f, "[{}]", t),
Ty::Never => write!(f, "!"),
Ty::Tuple(ts) => {
write!(f, "(")?;
for t in ts {
write!(f, "{},", t)?;
}
write!(f, ")")
}
Ty::Unknown => write!(f, "[unknown]")
}
}
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct InferenceResult {
type_for: FxHashMap<LocalSyntaxPtr, Ty>,
}
impl InferenceResult {
pub fn type_of_node(&self, node: SyntaxNodeRef) -> Option<Ty> {
self.type_for.get(&LocalSyntaxPtr::new(node)).cloned()
}
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct InferenceContext {
scopes: Arc<FnScopes>,

View file

@ -1,8 +1,11 @@
use std::fmt::Write;
use std::sync::Arc;
use std::path::{Path, PathBuf};
use salsa::Database;
use ra_db::{FilesDatabase, CrateGraph, SyntaxDatabase};
use ra_syntax::{SmolStr, algo::visit::{visitor, Visitor}, ast::{self, AstNode}};
use test_utils::{project_dir, dir_tests};
use relative_path::RelativePath;
use crate::{source_binder, mock::WORKSPACE, module::ModuleSourceNode};
@ -13,33 +16,46 @@ use crate::{
mock::MockDatabase,
};
fn infer_all_fns(fixture: &str) -> () {
let (db, source_root) = MockDatabase::with_files(fixture);
for &file_id in source_root.files.values() {
let source_file = db.source_file(file_id);
for fn_def in source_file.syntax().descendants().filter_map(ast::FnDef::cast) {
let func = source_binder::function_from_source(&db, file_id, fn_def).unwrap().unwrap();
let inference_result = func.infer(&db);
for (syntax_ptr, ty) in &inference_result.type_for {
let node = syntax_ptr.resolve(&source_file);
eprintln!("{} '{}': {:?}", syntax_ptr.range(), node.text(), ty);
}
fn infer_file(content: &str) -> String {
let (db, source_root, file_id) = MockDatabase::with_single_file(content);
let source_file = db.source_file(file_id);
let mut acc = String::new();
for fn_def in source_file.syntax().descendants().filter_map(ast::FnDef::cast) {
let func = source_binder::function_from_source(&db, file_id, fn_def).unwrap().unwrap();
let inference_result = func.infer(&db);
for (syntax_ptr, ty) in &inference_result.type_for {
let node = syntax_ptr.resolve(&source_file);
write!(acc, "{} '{}': {}\n", syntax_ptr.range(), ellipsize(node.text().to_string().replace("\n", " "), 15), ty);
}
}
acc
}
fn ellipsize(mut text: String, max_len: usize) -> String {
if text.len() <= max_len {
return text;
}
let ellipsis = "...";
let e_len = ellipsis.len();
let mut prefix_len = (max_len - e_len) / 2;
while !text.is_char_boundary(prefix_len) {
prefix_len += 1;
}
let mut suffix_len = max_len - e_len - prefix_len;
while !text.is_char_boundary(text.len() - suffix_len) {
suffix_len += 1;
}
text.replace_range(prefix_len..text.len() - suffix_len, ellipsis);
text
}
#[test]
fn infer_smoke_test() {
let text = "
//- /lib.rs
fn foo(x: u32, y: !) -> i128 {
x;
y;
return 1;
\"hello\";
0
}
";
infer_all_fns(text);
pub fn infer_tests() {
dir_tests(&test_data_dir(), &["."], |text, _path| {
infer_file(text)
});
}
fn test_data_dir() -> PathBuf {
project_dir().join("crates/ra_hir/src/ty/tests/data")
}

View file

@ -0,0 +1,11 @@
fn test(a: u32, b: isize, c: !, d: &str) {
a;
b;
c;
d;
1usize;
1isize;
"test";
1.0f32;
}

View file

@ -0,0 +1,13 @@
[33; 34) 'd': [unknown]
[88; 94) '1isize': [unknown]
[48; 49) 'a': u32
[55; 56) 'b': isize
[112; 118) '1.0f32': [unknown]
[76; 82) '1usize': [unknown]
[9; 10) 'a': u32
[27; 28) 'c': !
[62; 63) 'c': !
[17; 18) 'b': isize
[100; 106) '"test"': [unknown]
[42; 121) '{ ...f32; }': ()
[69; 70) 'd': [unknown]

View file

@ -9,6 +9,7 @@ use std::{
path::{Path, PathBuf, Component},
};
use test_utils::{project_dir, dir_tests, read_text, collect_tests};
use ra_syntax::{
utils::{check_fuzz_invariants, dump_tree},
SourceFileNode,
@ -16,7 +17,7 @@ use ra_syntax::{
#[test]
fn lexer_tests() {
dir_tests(&["lexer"], |text, _| {
dir_tests(&test_data_dir(), &["lexer"], |text, _| {
let tokens = ra_syntax::tokenize(text);
dump_tokens(&tokens, text)
})
@ -24,7 +25,7 @@ fn lexer_tests() {
#[test]
fn parser_tests() {
dir_tests(&["parser/inline/ok", "parser/ok"], |text, path| {
dir_tests(&test_data_dir(), &["parser/inline/ok", "parser/ok"], |text, path| {
let file = SourceFileNode::parse(text);
let errors = file.errors();
assert_eq!(
@ -35,7 +36,7 @@ fn parser_tests() {
);
dump_tree(file.syntax())
});
dir_tests(&["parser/err", "parser/inline/err"], |text, path| {
dir_tests(&test_data_dir(), &["parser/err", "parser/inline/err"], |text, path| {
let file = SourceFileNode::parse(text);
let errors = file.errors();
assert_ne!(
@ -50,7 +51,7 @@ fn parser_tests() {
#[test]
fn parser_fuzz_tests() {
for (_, text) in collect_tests(&["parser/fuzz-failures"]) {
for (_, text) in collect_tests(&test_data_dir(), &["parser/fuzz-failures"]) {
check_fuzz_invariants(&text)
}
}
@ -92,102 +93,6 @@ fn self_hosting_parsing() {
"self_hosting_parsing found too few files - is it running in the right directory?"
)
}
/// Read file and normalize newlines.
///
/// `rustc` seems to always normalize `\r\n` newlines to `\n`:
///
/// ```
/// let s = "
/// ";
/// assert_eq!(s.as_bytes(), &[10]);
/// ```
///
/// so this should always be correct.
fn read_text(path: &Path) -> String {
fs::read_to_string(path)
.expect(&format!("File at {:?} should be valid", path))
.replace("\r\n", "\n")
}
fn dir_tests<F>(paths: &[&str], f: F)
where
F: Fn(&str, &Path) -> String,
{
for (path, input_code) in collect_tests(paths) {
let parse_tree = f(&input_code, &path);
let path = path.with_extension("txt");
if !path.exists() {
println!("\nfile: {}", path.display());
println!("No .txt file with expected result, creating...\n");
println!("{}\n{}", input_code, parse_tree);
fs::write(&path, &parse_tree).unwrap();
panic!("No expected result")
}
let expected = read_text(&path);
let expected = expected.as_str();
let parse_tree = parse_tree.as_str();
assert_equal_text(expected, parse_tree, &path);
}
}
const REWRITE: bool = false;
fn assert_equal_text(expected: &str, actual: &str, path: &Path) {
if expected == actual {
return;
}
let dir = project_dir();
let pretty_path = path.strip_prefix(&dir).unwrap_or_else(|_| path);
if expected.trim() == actual.trim() {
println!("whitespace difference, rewriting");
println!("file: {}\n", pretty_path.display());
fs::write(path, actual).unwrap();
return;
}
if REWRITE {
println!("rewriting {}", pretty_path.display());
fs::write(path, actual).unwrap();
return;
}
assert_eq_text!(expected, actual, "file: {}", pretty_path.display());
}
fn collect_tests(paths: &[&str]) -> Vec<(PathBuf, String)> {
paths
.iter()
.flat_map(|path| {
let path = test_data_dir().join(path);
test_from_dir(&path).into_iter()
})
.map(|path| {
let text = read_text(&path);
(path, text)
})
.collect()
}
fn test_from_dir(dir: &Path) -> Vec<PathBuf> {
let mut acc = Vec::new();
for file in fs::read_dir(&dir).unwrap() {
let file = file.unwrap();
let path = file.path();
if path.extension().unwrap_or_default() == "rs" {
acc.push(path);
}
}
acc.sort();
acc
}
fn project_dir() -> PathBuf {
let dir = env!("CARGO_MANIFEST_DIR");
PathBuf::from(dir)
.parent()
.unwrap()
.parent()
.unwrap()
.to_owned()
}
fn test_data_dir() -> PathBuf {
project_dir().join("crates/ra_syntax/tests/data")

View file

@ -1,4 +1,6 @@
use std::fmt;
use std::fs;
use std::path::{Path, PathBuf};
use itertools::Itertools;
use text_unit::{TextRange, TextUnit};
@ -262,3 +264,100 @@ pub fn find_mismatch<'a>(expected: &'a Value, actual: &'a Value) -> Option<(&'a
_ => Some((expected, actual)),
}
}
pub fn dir_tests<F>(test_data_dir: &Path, paths: &[&str], f: F)
where
F: Fn(&str, &Path) -> String,
{
for (path, input_code) in collect_tests(test_data_dir, paths) {
let parse_tree = f(&input_code, &path);
let path = path.with_extension("txt");
if !path.exists() {
println!("\nfile: {}", path.display());
println!("No .txt file with expected result, creating...\n");
println!("{}\n{}", input_code, parse_tree);
fs::write(&path, &parse_tree).unwrap();
panic!("No expected result")
}
let expected = read_text(&path);
let expected = expected.as_str();
let parse_tree = parse_tree.as_str();
assert_equal_text(expected, parse_tree, &path);
}
}
pub fn collect_tests(test_data_dir: &Path, paths: &[&str]) -> Vec<(PathBuf, String)> {
paths
.iter()
.flat_map(|path| {
let path = test_data_dir.to_owned().join(path);
test_from_dir(&path).into_iter()
})
.map(|path| {
let text = read_text(&path);
(path, text)
})
.collect()
}
fn test_from_dir(dir: &Path) -> Vec<PathBuf> {
let mut acc = Vec::new();
for file in fs::read_dir(&dir).unwrap() {
let file = file.unwrap();
let path = file.path();
if path.extension().unwrap_or_default() == "rs" {
acc.push(path);
}
}
acc.sort();
acc
}
pub fn project_dir() -> PathBuf {
let dir = env!("CARGO_MANIFEST_DIR");
PathBuf::from(dir)
.parent()
.unwrap()
.parent()
.unwrap()
.to_owned()
}
/// Read file and normalize newlines.
///
/// `rustc` seems to always normalize `\r\n` newlines to `\n`:
///
/// ```
/// let s = "
/// ";
/// assert_eq!(s.as_bytes(), &[10]);
/// ```
///
/// so this should always be correct.
pub fn read_text(path: &Path) -> String {
fs::read_to_string(path)
.expect(&format!("File at {:?} should be valid", path))
.replace("\r\n", "\n")
}
const REWRITE: bool = false;
fn assert_equal_text(expected: &str, actual: &str, path: &Path) {
if expected == actual {
return;
}
let dir = project_dir();
let pretty_path = path.strip_prefix(&dir).unwrap_or_else(|_| path);
if expected.trim() == actual.trim() {
println!("whitespace difference, rewriting");
println!("file: {}\n", pretty_path.display());
fs::write(path, actual).unwrap();
return;
}
if REWRITE {
println!("rewriting {}", pretty_path.display());
fs::write(path, actual).unwrap();
return;
}
assert_eq_text!(expected, actual, "file: {}", pretty_path.display());
}