]> git.lizzy.rs Git - rust.git/commitdiff
Add testing infrastructure for type inference
authorFlorian Diebold <flodiebold@gmail.com>
Sun, 23 Dec 2018 11:05:54 +0000 (12:05 +0100)
committerFlorian Diebold <flodiebold@gmail.com>
Sun, 23 Dec 2018 12:48:04 +0000 (13:48 +0100)
 - move dir_tests to test_utils for that.

crates/ra_analysis/src/imp.rs
crates/ra_analysis/src/lib.rs
crates/ra_hir/src/mock.rs
crates/ra_hir/src/ty.rs
crates/ra_hir/src/ty/tests.rs
crates/ra_hir/src/ty/tests/data/0001_basics.rs [new file with mode: 0644]
crates/ra_hir/src/ty/tests/data/0001_basics.txt [new file with mode: 0644]
crates/ra_syntax/tests/test.rs
crates/test_utils/src/lib.rs

index b01382808ea8125234f851d0512f610792ef9310..4e0631679e5bd2a9e0cf57cae62a3b2680e0d137 100644 (file)
@@ -5,7 +5,8 @@
 
 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 @@ pub fn resolve_callable(
         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());
index 85df9c089c98efeacd532a11de04b29e116408d7..8308981405b52934ff9c911a845fd520ef41aa47 100644 (file)
@@ -366,6 +366,9 @@ pub fn resolve_callable(
     ) -> 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 {
index a9fa540d58de9b0393f93cd6a9a2a09f95e26931..3020ee793e46633395893629d7ddecb8103eb8fa 100644 (file)
@@ -24,6 +24,15 @@ pub(crate) fn with_files(fixture: &str) -> (MockDatabase, SourceRoot) {
         (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 ( <|> )");
index 087385b98f7fc0883739b636da97c184de897a2e..66b204dcd706dcaa4705517e1fa0ecfe7a37e0d0 100644 (file)
@@ -6,6 +6,7 @@
 
 use std::sync::Arc;
 use std::collections::HashMap;
+use std::fmt;
 
 use ra_db::LocalSyntaxPtr;
 use ra_syntax::{
@@ -184,11 +185,40 @@ pub fn unit() -> Self {
     }
 }
 
+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>,
index f2466dd517eb5dfb206e2a134f5036cb82ce55c4..98eedaa3f9cf40d04c7b98227c9beb35003e4a44 100644 (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};
     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
-        }
-    ";
+pub fn infer_tests() {
+    dir_tests(&test_data_dir(), &["."], |text, _path| {
+        infer_file(text)
+    });
+}
 
-    infer_all_fns(text);
+fn test_data_dir() -> PathBuf {
+    project_dir().join("crates/ra_hir/src/ty/tests/data")
 }
diff --git a/crates/ra_hir/src/ty/tests/data/0001_basics.rs b/crates/ra_hir/src/ty/tests/data/0001_basics.rs
new file mode 100644 (file)
index 0000000..59a60d0
--- /dev/null
@@ -0,0 +1,11 @@
+
+fn test(a: u32, b: isize, c: !, d: &str) {
+    a;
+    b;
+    c;
+    d;
+    1usize;
+    1isize;
+    "test";
+    1.0f32;
+}
diff --git a/crates/ra_hir/src/ty/tests/data/0001_basics.txt b/crates/ra_hir/src/ty/tests/data/0001_basics.txt
new file mode 100644 (file)
index 0000000..0c46f24
--- /dev/null
@@ -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]
index 4266864bdfc29b5734de1cabdbe7777561ea9178..9d94a1a23c3aca1b69a4bb5c9cbef9ce5664edb3 100644 (file)
@@ -9,6 +9,7 @@
     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 @@
 
 #[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")
index beb936c616d3a5d7cf0cb9517a514caf5bade9e5..012b1d0b40218dd3889be719c80ae246bba92a3a 100644 (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());
+}