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