Skip to main content

mz_deploy/lsp/
references.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10//! Find-references for SQL identifiers.
11//!
12//! Given a cursor position on an identifier, finds all project objects that
13//! **depend on** the referenced object. This is the inverse of go-to-definition:
14//! where go-to-definition answers "where is this defined?", find-references
15//! answers "who uses this?"
16//!
17//! ## Algorithm
18//!
19//! 1. Resolve identifier parts to an `ObjectId` (reuses
20//!    [`goto_definition::resolve_object_id`]).
21//! 2. Query [`ProjectCache::get_dependents`] to find all objects that depend on
22//!    the target.
23//! 3. For each dependent, look up its source file path via
24//!    [`ProjectCache::get_object`] and return an LSP [`Location`].
25//!
26//! ## Includes the definition
27//!
28//! When `include_declaration` is true (standard LSP behavior), the defining
29//! file is prepended to the results so the user sees the full picture.
30
31use crate::project::compiler::cache::ProjectCache;
32use std::path::Path;
33use tower_lsp::lsp_types::{Location, Range, Url};
34
35use super::goto_definition;
36
37/// Find all project objects that reference the identified object.
38///
39/// Queries the [`ProjectCache`] for dependents of the resolved object. Returns
40/// a [`Location`] for each dependent's source file. If `include_declaration` is
41/// true, the defining file itself is included as the first result.
42///
43/// Returns an empty vec if the identifier cannot be resolved or has no
44/// dependents.
45pub(super) fn find_references(
46    parts: &[String],
47    file_uri: &Url,
48    root: &Path,
49    project_cache: &ProjectCache,
50    include_declaration: bool,
51) -> Vec<Location> {
52    let id = match goto_definition::resolve_object_id(parts, file_uri, root) {
53        Some(id) => id,
54        None => return Vec::new(),
55    };
56
57    let mut locations = Vec::new();
58
59    if include_declaration {
60        if let Some(cached_obj) = project_cache.get_object(&id) {
61            if let Some(loc) = file_location(root, &cached_obj.file_path) {
62                locations.push(loc);
63            }
64        }
65    }
66
67    for dep_id in project_cache.get_dependents(&id) {
68        if let Some(cached_obj) = project_cache.get_object(&dep_id) {
69            if let Some(loc) = file_location(root, &cached_obj.file_path) {
70                locations.push(loc);
71            }
72        }
73    }
74
75    locations
76}
77
78/// Build a [`Location`] pointing to the start of a source file.
79fn file_location(root: &Path, file_path: &str) -> Option<Location> {
80    let full_path = root.join(file_path);
81    let uri = Url::from_file_path(&full_path).ok()?;
82    Some(Location {
83        uri,
84        range: Range::default(),
85    })
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91    use crate::project::compiler::cache::ProjectCache;
92    use std::path::Path;
93
94    #[cfg_attr(miri, ignore)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
95    #[mz_ore::test]
96    fn object_with_dependents() {
97        let (root, cache) = build_test_project_cache();
98        let file_uri = Url::from_file_path(root.path().join("models/mydb/public/bar.sql")).unwrap();
99
100        // "foo" is referenced by "bar"
101        let locations =
102            find_references(&["foo".to_string()], &file_uri, root.path(), &cache, false);
103        assert_eq!(locations.len(), 1);
104        let expected = Url::from_file_path(root.path().join("models/mydb/public/bar.sql")).unwrap();
105        assert_eq!(locations[0].uri, expected);
106    }
107
108    #[cfg_attr(miri, ignore)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
109    #[mz_ore::test]
110    fn object_with_dependents_include_declaration() {
111        let (root, cache) = build_test_project_cache();
112        let file_uri = Url::from_file_path(root.path().join("models/mydb/public/bar.sql")).unwrap();
113
114        let locations = find_references(&["foo".to_string()], &file_uri, root.path(), &cache, true);
115        assert_eq!(locations.len(), 2);
116        // First result is the definition itself.
117        let def_uri = Url::from_file_path(root.path().join("models/mydb/public/foo.sql")).unwrap();
118        assert_eq!(locations[0].uri, def_uri);
119    }
120
121    #[cfg_attr(miri, ignore)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
122    #[mz_ore::test]
123    fn object_with_no_dependents() {
124        let (root, cache) = build_test_project_cache();
125        let file_uri = Url::from_file_path(root.path().join("models/mydb/public/foo.sql")).unwrap();
126
127        // "bar" has no dependents
128        let locations =
129            find_references(&["bar".to_string()], &file_uri, root.path(), &cache, false);
130        assert!(locations.is_empty());
131    }
132
133    #[cfg_attr(miri, ignore)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
134    #[mz_ore::test]
135    fn unknown_identifier_returns_empty() {
136        let (root, cache) = build_test_project_cache();
137        let file_uri = Url::from_file_path(root.path().join("models/mydb/public/bar.sql")).unwrap();
138
139        let locations = find_references(
140            &["nonexistent".to_string()],
141            &file_uri,
142            root.path(),
143            &cache,
144            false,
145        );
146        assert!(locations.is_empty());
147    }
148
149    #[cfg_attr(miri, ignore)] // unsupported operation: can't call foreign function `rust_psm_stack_pointer` on OS `linux`
150    #[mz_ore::test]
151    fn transitive_dependents() {
152        let (root, cache) = build_chain_project_cache();
153        let file_uri = Url::from_file_path(root.path().join("models/mydb/public/c.sql")).unwrap();
154
155        // "a" is depended on by "b" (directly), not "c" (c depends on b, not a)
156        let locations = find_references(&["a".to_string()], &file_uri, root.path(), &cache, false);
157        assert_eq!(locations.len(), 1);
158        let expected = Url::from_file_path(root.path().join("models/mydb/public/b.sql")).unwrap();
159        assert_eq!(locations[0].uri, expected);
160    }
161
162    /// Compile a project and open a ProjectCache from its SQLite DB.
163    fn build_test_project_cache() -> (tempfile::TempDir, ProjectCache) {
164        let root = tempfile::tempdir().unwrap();
165        let models = root.path().join("models/mydb/public");
166        std::fs::create_dir_all(&models).unwrap();
167        std::fs::write(models.join("foo.sql"), "CREATE VIEW foo AS SELECT 1 AS id;").unwrap();
168        std::fs::write(
169            models.join("bar.sql"),
170            "CREATE VIEW bar AS SELECT * FROM foo;",
171        )
172        .unwrap();
173        write_project_toml(root.path());
174
175        let _project = crate::project::plan_sync(
176            &crate::fs::FileSystem::new(),
177            root.path(),
178            None,
179            None,
180            &Default::default(),
181        )
182        .expect("project should compile");
183        let cache = ProjectCache::open(root.path(), "", None, &Default::default())
184            .expect("cache should open")
185            .expect("cache DB should exist");
186        (root, cache)
187    }
188
189    /// Compile a chain project (a -> b -> c) and open a ProjectCache.
190    fn build_chain_project_cache() -> (tempfile::TempDir, ProjectCache) {
191        let root = tempfile::tempdir().unwrap();
192        let models = root.path().join("models/mydb/public");
193        std::fs::create_dir_all(&models).unwrap();
194        std::fs::write(models.join("a.sql"), "CREATE VIEW a AS SELECT 1 AS id;").unwrap();
195        std::fs::write(models.join("b.sql"), "CREATE VIEW b AS SELECT * FROM a;").unwrap();
196        std::fs::write(models.join("c.sql"), "CREATE VIEW c AS SELECT * FROM b;").unwrap();
197        write_project_toml(root.path());
198
199        let _project = crate::project::plan_sync(
200            &crate::fs::FileSystem::new(),
201            root.path(),
202            None,
203            None,
204            &Default::default(),
205        )
206        .expect("project should compile");
207        let cache = ProjectCache::open(root.path(), "", None, &Default::default())
208            .expect("cache should open")
209            .expect("cache DB should exist");
210        (root, cache)
211    }
212
213    fn write_project_toml(root: &Path) {
214        std::fs::write(root.join("project.toml"), "[project]\nname = \"test\"\n").unwrap();
215    }
216}