Skip to main content

lintel_check/parsers/
mod.rs

1mod json;
2mod json5;
3mod jsonc;
4mod markdown;
5mod toml_parser;
6mod yaml;
7
8use std::path::Path;
9
10use serde_json::Value;
11
12use crate::diagnostics::ParseDiagnostic;
13
14pub use self::json::JsonParser;
15pub use self::json5::Json5Parser;
16pub use self::jsonc::JsoncParser;
17pub use self::markdown::MarkdownParser;
18pub use self::toml_parser::TomlParser;
19pub use self::yaml::YamlParser;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum FileFormat {
23    Json,
24    Json5,
25    Jsonc,
26    Toml,
27    Yaml,
28    Markdown,
29}
30
31/// Parse file content into a `serde_json::Value`.
32///
33/// Implementations must produce a [`ParseDiagnostic`] with an accurate source
34/// span when parsing fails.
35pub trait Parser {
36    /// # Errors
37    ///
38    /// Returns a [`ParseDiagnostic`] with an accurate source span when parsing fails.
39    fn parse(&self, content: &str, file_name: &str) -> Result<Value, ParseDiagnostic>;
40
41    /// Extract the `$schema` URI from file content and/or parsed value.
42    ///
43    /// The default implementation reads `value["$schema"]`, which works for
44    /// JSON, JSON5, and JSONC. YAML and TOML override this to handle their
45    /// format-specific conventions (modeline comments, etc.).
46    fn extract_schema_uri(&self, _content: &str, value: &Value) -> Option<String> {
47        value
48            .get("$schema")
49            .and_then(Value::as_str)
50            .map(String::from)
51    }
52}
53
54/// Detect file format from extension. Returns `None` for unrecognized extensions.
55pub fn detect_format(path: &Path) -> Option<FileFormat> {
56    match path.extension().and_then(|e| e.to_str()) {
57        Some("json") => Some(FileFormat::Json),
58        Some("yaml" | "yml") => Some(FileFormat::Yaml),
59        Some("json5") => Some(FileFormat::Json5),
60        Some("jsonc") => Some(FileFormat::Jsonc),
61        Some("toml") => Some(FileFormat::Toml),
62        Some("md" | "mdx") => Some(FileFormat::Markdown),
63        _ => None,
64    }
65}
66
67/// Return a boxed parser for the given format.
68pub fn parser_for(format: FileFormat) -> Box<dyn Parser> {
69    match format {
70        FileFormat::Json => Box::new(JsonParser),
71        FileFormat::Json5 => Box::new(Json5Parser),
72        FileFormat::Jsonc => Box::new(JsoncParser),
73        FileFormat::Toml => Box::new(TomlParser),
74        FileFormat::Yaml => Box::new(YamlParser),
75        FileFormat::Markdown => Box::new(MarkdownParser),
76    }
77}
78
79/// Convert 1-based line and column to a byte offset in content.
80pub fn line_col_to_offset(content: &str, line: usize, col: usize) -> usize {
81    let mut offset = 0;
82    for (i, l) in content.lines().enumerate() {
83        if i + 1 == line {
84            return offset + col.saturating_sub(1);
85        }
86        offset += l.len() + 1; // +1 for newline
87    }
88    offset.min(content.len())
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94
95    // --- detect_format ---
96
97    #[test]
98    fn detect_format_json() {
99        assert_eq!(detect_format(Path::new("foo.json")), Some(FileFormat::Json));
100    }
101
102    #[test]
103    fn detect_format_yaml() {
104        assert_eq!(detect_format(Path::new("foo.yaml")), Some(FileFormat::Yaml));
105        assert_eq!(detect_format(Path::new("foo.yml")), Some(FileFormat::Yaml));
106    }
107
108    #[test]
109    fn detect_format_json5() {
110        assert_eq!(
111            detect_format(Path::new("foo.json5")),
112            Some(FileFormat::Json5)
113        );
114    }
115
116    #[test]
117    fn detect_format_jsonc() {
118        assert_eq!(
119            detect_format(Path::new("foo.jsonc")),
120            Some(FileFormat::Jsonc)
121        );
122    }
123
124    #[test]
125    fn detect_format_toml() {
126        assert_eq!(detect_format(Path::new("foo.toml")), Some(FileFormat::Toml));
127    }
128
129    #[test]
130    fn detect_format_unknown_returns_none() {
131        assert_eq!(detect_format(Path::new("foo.txt")), None);
132        assert_eq!(detect_format(Path::new("foo")), None);
133        assert_eq!(detect_format(Path::new("devenv.nix")), None);
134    }
135
136    // --- extract_schema_uri via trait ---
137
138    #[test]
139    fn extract_schema_json_with_schema() {
140        let val = serde_json::json!({"$schema": "https://example.com/s.json"});
141        let uri = JsonParser.extract_schema_uri("", &val);
142        assert_eq!(uri.as_deref(), Some("https://example.com/s.json"));
143    }
144
145    #[test]
146    fn extract_schema_json_without_schema() {
147        let val = serde_json::json!({"key": "value"});
148        let uri = JsonParser.extract_schema_uri("", &val);
149        assert!(uri.is_none());
150    }
151
152    #[test]
153    fn extract_schema_json5_with_schema() {
154        let val = serde_json::json!({"$schema": "https://example.com/s.json"});
155        let uri = Json5Parser.extract_schema_uri("", &val);
156        assert_eq!(uri.as_deref(), Some("https://example.com/s.json"));
157    }
158
159    #[test]
160    fn extract_schema_jsonc_with_schema() {
161        let val = serde_json::json!({"$schema": "https://example.com/s.json"});
162        let uri = JsoncParser.extract_schema_uri("", &val);
163        assert_eq!(uri.as_deref(), Some("https://example.com/s.json"));
164    }
165
166    #[test]
167    fn extract_schema_yaml_modeline() {
168        let content = "# yaml-language-server: $schema=https://example.com/s.json\nkey: value\n";
169        let val = serde_json::json!({"key": "value"});
170        let uri = YamlParser.extract_schema_uri(content, &val);
171        assert_eq!(uri.as_deref(), Some("https://example.com/s.json"));
172    }
173
174    #[test]
175    fn extract_schema_yaml_modeline_with_leading_blank_lines() {
176        let content = "\n# yaml-language-server: $schema=https://example.com/s.json\nkey: value\n";
177        let val = serde_json::json!({"key": "value"});
178        let uri = YamlParser.extract_schema_uri(content, &val);
179        assert_eq!(uri.as_deref(), Some("https://example.com/s.json"));
180    }
181
182    #[test]
183    fn extract_schema_yaml_modeline_after_other_comment() {
184        let content = "# some comment\n# yaml-language-server: $schema=https://example.com/s.json\nkey: value\n";
185        let val = serde_json::json!({"key": "value"});
186        let uri = YamlParser.extract_schema_uri(content, &val);
187        assert_eq!(uri.as_deref(), Some("https://example.com/s.json"));
188    }
189
190    #[test]
191    fn extract_schema_yaml_modeline_not_in_body() {
192        let content = "key: value\n# yaml-language-server: $schema=https://example.com/s.json\n";
193        let val = serde_json::json!({"key": "value"});
194        let uri = YamlParser.extract_schema_uri(content, &val);
195        assert!(uri.is_none());
196    }
197
198    #[test]
199    fn extract_schema_yaml_top_level_property() {
200        let content = "$schema: https://example.com/s.json\nkey: value\n";
201        let val = serde_json::json!({"$schema": "https://example.com/s.json", "key": "value"});
202        let uri = YamlParser.extract_schema_uri(content, &val);
203        assert_eq!(uri.as_deref(), Some("https://example.com/s.json"));
204    }
205
206    #[test]
207    fn extract_schema_yaml_modeline_takes_priority() {
208        let content = "# yaml-language-server: $schema=https://modeline.com/s.json\n$schema: https://property.com/s.json\n";
209        let val = serde_json::json!({"$schema": "https://property.com/s.json"});
210        let uri = YamlParser.extract_schema_uri(content, &val);
211        assert_eq!(uri.as_deref(), Some("https://modeline.com/s.json"));
212    }
213
214    #[test]
215    fn extract_schema_yaml_none() {
216        let content = "key: value\n";
217        let val = serde_json::json!({"key": "value"});
218        let uri = YamlParser.extract_schema_uri(content, &val);
219        assert!(uri.is_none());
220    }
221
222    // --- TOML schema extraction ---
223
224    #[test]
225    fn extract_schema_toml_comment() {
226        let content = "# $schema: https://example.com/s.json\nkey = \"value\"\n";
227        let val = serde_json::json!({"key": "value"});
228        let uri = TomlParser.extract_schema_uri(content, &val);
229        assert_eq!(uri.as_deref(), Some("https://example.com/s.json"));
230    }
231
232    #[test]
233    fn extract_schema_toml_with_leading_blank_lines() {
234        let content = "\n# $schema: https://example.com/s.json\nkey = \"value\"\n";
235        let val = serde_json::json!({"key": "value"});
236        let uri = TomlParser.extract_schema_uri(content, &val);
237        assert_eq!(uri.as_deref(), Some("https://example.com/s.json"));
238    }
239
240    #[test]
241    fn extract_schema_toml_not_in_body() {
242        let content = "key = \"value\"\n# $schema: https://example.com/s.json\n";
243        let val = serde_json::json!({"key": "value"});
244        let uri = TomlParser.extract_schema_uri(content, &val);
245        assert!(uri.is_none());
246    }
247
248    #[test]
249    fn extract_schema_toml_none() {
250        let content = "key = \"value\"\n";
251        let val = serde_json::json!({"key": "value"});
252        let uri = TomlParser.extract_schema_uri(content, &val);
253        assert!(uri.is_none());
254    }
255
256    // --- line_col_to_offset ---
257
258    #[test]
259    fn line_col_to_offset_first_line() {
260        assert_eq!(line_col_to_offset("hello\nworld", 1, 1), 0);
261        assert_eq!(line_col_to_offset("hello\nworld", 1, 3), 2);
262    }
263
264    #[test]
265    fn line_col_to_offset_second_line() {
266        assert_eq!(line_col_to_offset("hello\nworld", 2, 1), 6);
267        assert_eq!(line_col_to_offset("hello\nworld", 2, 3), 8);
268    }
269
270    // --- parser_for round-trip ---
271
272    #[test]
273    fn parser_for_json_parses() -> anyhow::Result<()> {
274        let p = parser_for(FileFormat::Json);
275        let val = p.parse(r#"{"key":"value"}"#, "test.json")?;
276        assert_eq!(val, serde_json::json!({"key": "value"}));
277        Ok(())
278    }
279
280    #[test]
281    fn parser_for_yaml_parses() -> anyhow::Result<()> {
282        let p = parser_for(FileFormat::Yaml);
283        let val = p.parse("key: value\n", "test.yaml")?;
284        assert_eq!(val, serde_json::json!({"key": "value"}));
285        Ok(())
286    }
287
288    #[test]
289    fn parser_for_json5_parses() -> anyhow::Result<()> {
290        let p = parser_for(FileFormat::Json5);
291        let val = p.parse(r#"{key: "value"}"#, "test.json5")?;
292        assert_eq!(val, serde_json::json!({"key": "value"}));
293        Ok(())
294    }
295
296    #[test]
297    fn parser_for_jsonc_parses() -> anyhow::Result<()> {
298        let p = parser_for(FileFormat::Jsonc);
299        let val = p.parse(r#"{"key": "value" /* comment */}"#, "test.jsonc")?;
300        assert_eq!(val, serde_json::json!({"key": "value"}));
301        Ok(())
302    }
303
304    #[test]
305    fn parser_for_toml_parses() -> anyhow::Result<()> {
306        let p = parser_for(FileFormat::Toml);
307        let val = p.parse("key = \"value\"\n", "test.toml")?;
308        assert_eq!(val, serde_json::json!({"key": "value"}));
309        Ok(())
310    }
311}