shader_sense/
lib.rs

1//! Shader sense is a library for runtime validation and symbol inspection that can handle multiple shader languages, primarily intended for use in a language server. This works through the use of standard API for validation and tree-sitter for symbol inspection. It can be built to desktop or [WASI](https://wasi.dev/). WASI will let the extension run even in browser, but it suffer from limitations. See below for more informations.
2//!
3//! For symbol inspection, the API is relying on abstract syntax tree. As we want to support different language, and to ease this process, we are using the [`tree-sitter`] API (instead of standard API), which generate AST with query support, and is already available in a lot of languages.
4//!
5//! # Validating shader
6//!
7//! Validating shader is using standard API behind the hood :
8//! - **GLSL** uses [`glslang`] as backend. It provide complete linting for GLSL trough glslang API bindings from C.
9//! - **HLSL** uses [`hassle-rs`] as backend. It provides bindings to directx shader compiler in rust.
10//! - **WGSL** uses [`naga`] as backend for linting.
11//!
12//! ```no_run
13//! use shader_sense::validator::validator::Validator;
14//! use shader_sense::shader::ShaderParams;
15//! use std::path::Path;
16//! let shader_path = Path::new("/path/to/shader.hlsl");
17//! let shader_content = std::fs::read_to_string(shader_path).unwrap();
18//! let validator = Validator::hlsl();
19//! match validator.validate_shader(
20//!     &shader_content,
21//!     shader_path,
22//!     &ShaderParams::default(),
23//!     &mut |path: &Path| Some(std::fs::read_to_string(path).unwrap()),
24//! ) {
25//!     Ok(diagnostic_list) => println!(
26//!         "Validated file and return following diagnostics: {:#?}",
27//!         diagnostic_list
28//!     ),
29//!     Err(err) => println!("Failed to validate file: {:#?}", err),
30//! }
31//! ```
32//!
33//! # Inspecting shader
34//!
35//! You can inspect shaders aswell to find symbols inside it, their position and informations. It is using the [`tree-sitter`] API (instead of standard API) for performances reason and also because most standard API do not expose easily their AST.
36//!
37//! ```no_run
38//! use shader_sense::shader::{ShaderParams, HlslShadingLanguageTag};
39//! use shader_sense::symbols::{
40//!     shader_module_parser::ShaderModuleParser,
41//!     symbol_provider::SymbolProvider,
42//!     symbol_provider::default_include_callback
43//! };
44//! use std::path::Path;
45//! let shader_path = Path::new("/path/to/shader.hlsl");
46//! let shader_content = std::fs::read_to_string(shader_path).unwrap();
47//! let mut shader_module_parser = ShaderModuleParser::hlsl();
48//! let symbol_provider = SymbolProvider::hlsl();
49//! match shader_module_parser.create_module(shader_path, &shader_content) {
50//!     Ok(shader_module) => {
51//!         let symbols = symbol_provider
52//!             .query_symbols(
53//!                 &shader_module,
54//!                 ShaderParams::default(),
55//!                 &mut default_include_callback::<HlslShadingLanguageTag>,
56//!                 None,
57//!             )
58//!             .unwrap();
59//!         let symbol_list = symbols.get_all_symbols();
60//!         println!("Found symbols: {:#?}", symbol_list);
61//!     }
62//!     Err(err) => println!("Failed to create ast: {:#?}", err),
63//! }
64//! ```
65
66pub mod include;
67pub mod position;
68pub mod shader;
69pub mod shader_error;
70pub mod symbols;
71pub mod validator;
72
73#[cfg(test)]
74mod tests {
75    use std::{
76        cell::RefCell,
77        collections::HashMap,
78        path::{Path, PathBuf},
79        rc::Rc,
80    };
81
82    use crate::{
83        include::{canonicalize, IncludeHandler},
84        shader::{ShaderParams, ShadingLanguage},
85        symbols::{shader_module_parser::ShaderModuleParser, symbol_provider::SymbolProvider},
86        validator::validator::Validator,
87    };
88
89    fn validate_include(path: &Path) -> bool {
90        let file_path = Path::new("./test/hlsl/dontcare.hlsl");
91        let mut include_handler = IncludeHandler::main(
92            file_path,
93            vec![],
94            HashMap::from([
95                (
96                    PathBuf::from("/Packages"),
97                    PathBuf::from("./test/hlsl/inc0/inc1"),
98                ),
99                (
100                    PathBuf::from("Packages"),
101                    PathBuf::from("./test/hlsl/inc0/inc1"),
102                ),
103                (
104                    PathBuf::from("Using\\Backslashes"),
105                    PathBuf::from("./test/hlsl/inc0/inc1"),
106                ),
107            ]),
108        );
109        include_handler.search_path_in_includes(path).is_some()
110    }
111
112    #[test]
113    fn test_virtual_path() {
114        assert!(
115            validate_include(Path::new("/Packages/level1.hlsl")),
116            "Virtual path with prefix failed."
117        );
118        assert!(
119            validate_include(Path::new("Packages/level1.hlsl")),
120            "Virtual path without prefix failed."
121        );
122        #[cfg(target_os = "windows")] // Only windows support backslashes.
123        assert!(
124            validate_include(Path::new("Using/Backslashes/level1.hlsl")),
125            "Virtual path with backslash failed."
126        );
127    }
128
129    #[test]
130    fn test_directory_stack() {
131        let file_path = Path::new("./test/hlsl/include-level.hlsl");
132        let mut include_handler = IncludeHandler::main(file_path, vec![], HashMap::new());
133        let absolute_level0 =
134            include_handler.search_path_in_includes(Path::new("./inc0/level0.hlsl"));
135        assert!(absolute_level0.is_some());
136        include_handler.push_directory_stack(&absolute_level0.unwrap());
137        let absolute_level1 =
138            include_handler.search_path_in_includes(Path::new("./inc1/level1.hlsl"));
139        assert!(absolute_level1.is_some());
140    }
141
142    #[test]
143    fn test_stack_overflow() {
144        // Should handle include stack overflow gracefully.
145        let file_path = Path::new("./test/hlsl/stack-overflow.hlsl");
146        let mut shader_module_parser =
147            ShaderModuleParser::from_shading_language(ShadingLanguage::Hlsl);
148        let symbol_provider = SymbolProvider::from_shading_language(ShadingLanguage::Hlsl);
149        let shader_module = shader_module_parser
150            .create_module(file_path, &std::fs::read_to_string(file_path).unwrap())
151            .unwrap();
152        println!("Testing symbol overflow");
153        let mut depth = 0;
154        match symbol_provider.query_symbols(
155            &shader_module,
156            ShaderParams::default(),
157            &mut |include| {
158                depth += 1;
159                println!(
160                    "Including {} (depth {})",
161                    include.get_absolute_path().display(),
162                    depth
163                );
164                Ok(Some(Rc::new(RefCell::new(
165                    shader_module_parser
166                        .create_module(
167                            &include.get_absolute_path(),
168                            &std::fs::read_to_string(&include.get_absolute_path()).unwrap(),
169                        )
170                        .unwrap(),
171                ))))
172            },
173            None,
174        ) {
175            Ok(_) => {}
176            Err(err) => panic!("Failed to query symbols: {}", err),
177        }
178        println!("Testing validation overflow");
179        let validator = Validator::from_shading_language(ShadingLanguage::Hlsl);
180        match validator.validate_shader(
181            &shader_module.content,
182            file_path,
183            &ShaderParams::default(),
184            &mut |path| Some(std::fs::read_to_string(path).unwrap()),
185        ) {
186            Ok(diagnostics) => assert!(
187                !diagnostics.is_empty(),
188                "Diagnostics are empty but should not be."
189            ),
190            Err(err) => panic!("Failed to validate shader: {}", err),
191        }
192    }
193    #[test]
194    fn test_canonicalize_parent() {
195        if cfg!(target_os = "windows") {
196            let path = canonicalize(Path::new("D:\\test\\data")).unwrap();
197            assert!(path == Path::new("D:\\test\\data"));
198            assert!(path.parent().unwrap() == Path::new("D:\\test"));
199        } else {
200            let path = canonicalize(Path::new("/test/data")).unwrap();
201            assert!(path == Path::new("/test/data"));
202            assert!(path.parent().unwrap() == Path::new("/test"));
203        }
204    }
205    #[test]
206    fn test_canonicalize_join() {
207        if cfg!(target_os = "windows") {
208            let path = canonicalize(Path::new("D:\\test")).unwrap();
209            assert!(path == Path::new("D:\\test"));
210            assert!(path.join("data") == Path::new("D:\\test\\data"));
211        } else {
212            let path = canonicalize(Path::new("/test")).unwrap();
213            assert!(path == Path::new("/test"));
214            assert!(path.join("data") == Path::new("/test/data"));
215        }
216    }
217}