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}