Skip to main content

sqry_core/graph/
node.rs

1//! Node types for the unified code graph
2//!
3//! This module defines the core node types that represent code entities
4//! (functions, classes, modules, etc.) in the unified graph architecture.
5
6use serde::{Deserialize, Serialize};
7use std::fmt;
8use std::sync::Arc;
9
10/// Language identifier
11#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
12pub enum Language {
13    /// C language
14    C,
15    /// C++ language
16    Cpp,
17    /// C# language
18    CSharp,
19    /// CSS language
20    Css,
21    /// JavaScript language
22    JavaScript,
23    /// Python language
24    Python,
25    /// TypeScript language
26    TypeScript,
27    /// Rust language
28    Rust,
29    /// Go language
30    Go,
31    /// Java language
32    Java,
33    /// Ruby language
34    Ruby,
35    /// PHP language
36    Php,
37    /// Swift language
38    Swift,
39    /// Kotlin language
40    Kotlin,
41    /// Scala language
42    Scala,
43    /// SQL language
44    Sql,
45    /// Dart language
46    Dart,
47    /// Lua language
48    Lua,
49    /// Perl language
50    Perl,
51    /// Shell (Bash) language
52    Shell,
53    /// Groovy language
54    Groovy,
55    /// Elixir language
56    Elixir,
57    /// R language
58    R,
59    /// Haskell language
60    Haskell,
61    /// HTML language
62    Html,
63    /// Svelte language
64    Svelte,
65    /// Vue language
66    Vue,
67    /// Zig language
68    Zig,
69    /// Terraform (HCL) language
70    Terraform,
71    /// Puppet language
72    Puppet,
73    /// Pulumi language
74    Pulumi,
75    /// Virtual language for HTTP endpoints
76    Http,
77    /// Oracle PL/SQL language
78    Plsql,
79    /// Salesforce Apex language
80    Apex,
81    /// SAP ABAP language
82    Abap,
83    /// `ServiceNow` (Xanadu) language
84    ServiceNow,
85    /// JSON configuration files
86    Json,
87}
88
89impl fmt::Display for Language {
90    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
91        // Delegates to `short_name` so the emitted spelling has exactly one
92        // definition. This is the legacy wire form (`ts`, `js`, `py`) and is
93        // deliberately unchanged: manifest confidence keys and textual
94        // `NodeId` both persist it (issue #714).
95        f.write_str(self.short_name())
96    }
97}
98
99impl Language {
100    /// Every language variant, in declaration order.
101    ///
102    /// The single list every generated surface derives from: the query
103    /// field registry's accepted values, validation help text, and the
104    /// documented language table. Hand-maintained copies drift, this does
105    /// not.
106    pub const ALL: &'static [Self] = &[
107        Self::C,
108        Self::Cpp,
109        Self::CSharp,
110        Self::Css,
111        Self::JavaScript,
112        Self::Python,
113        Self::TypeScript,
114        Self::Rust,
115        Self::Go,
116        Self::Java,
117        Self::Ruby,
118        Self::Php,
119        Self::Swift,
120        Self::Kotlin,
121        Self::Scala,
122        Self::Sql,
123        Self::Dart,
124        Self::Lua,
125        Self::Perl,
126        Self::Shell,
127        Self::Groovy,
128        Self::Elixir,
129        Self::R,
130        Self::Haskell,
131        Self::Html,
132        Self::Svelte,
133        Self::Vue,
134        Self::Zig,
135        Self::Terraform,
136        Self::Puppet,
137        Self::Pulumi,
138        Self::Http,
139        Self::Plsql,
140        Self::Apex,
141        Self::Abap,
142        Self::ServiceNow,
143        Self::Json,
144    ];
145
146    /// The canonical machine identifier for this language.
147    ///
148    /// This is the vocabulary every query predicate and machine-facing
149    /// filter speaks (`typescript`, not `ts`). Distinct from
150    /// [`Self::short_name`], which is the legacy display spelling.
151    #[must_use]
152    pub const fn canonical_name(self) -> &'static str {
153        match self {
154            Self::C => "c",
155            Self::Cpp => "cpp",
156            Self::CSharp => "csharp",
157            Self::Css => "css",
158            Self::JavaScript => "javascript",
159            Self::Python => "python",
160            Self::TypeScript => "typescript",
161            Self::Rust => "rust",
162            Self::Go => "go",
163            Self::Java => "java",
164            Self::Ruby => "ruby",
165            Self::Php => "php",
166            Self::Swift => "swift",
167            Self::Kotlin => "kotlin",
168            Self::Scala => "scala",
169            Self::Sql => "sql",
170            Self::Dart => "dart",
171            Self::Lua => "lua",
172            Self::Perl => "perl",
173            Self::Shell => "shell",
174            Self::Groovy => "groovy",
175            Self::Elixir => "elixir",
176            Self::R => "r",
177            Self::Haskell => "haskell",
178            Self::Html => "html",
179            Self::Svelte => "svelte",
180            Self::Vue => "vue",
181            Self::Zig => "zig",
182            Self::Terraform => "terraform",
183            Self::Puppet => "puppet",
184            Self::Pulumi => "pulumi",
185            Self::Http => "http",
186            Self::Plsql => "plsql",
187            Self::Apex => "apex",
188            Self::Abap => "abap",
189            Self::ServiceNow => "servicenow",
190            Self::Json => "json",
191        }
192    }
193
194    /// The short display spelling, as emitted by [`fmt::Display`].
195    ///
196    /// Differs from [`Self::canonical_name`] only for JavaScript (`js`),
197    /// Python (`py`), and TypeScript (`ts`). Kept as a separate concept
198    /// because it is persisted in graph manifests and textual node ids, so
199    /// it cannot be changed without a migration.
200    #[must_use]
201    pub const fn short_name(self) -> &'static str {
202        match self {
203            Self::C => "c",
204            Self::Cpp => "cpp",
205            Self::CSharp => "csharp",
206            Self::Css => "css",
207            Self::JavaScript => "js",
208            Self::Python => "py",
209            Self::TypeScript => "ts",
210            Self::Rust => "rust",
211            Self::Go => "go",
212            Self::Java => "java",
213            Self::Ruby => "ruby",
214            Self::Php => "php",
215            Self::Swift => "swift",
216            Self::Kotlin => "kotlin",
217            Self::Scala => "scala",
218            Self::Sql => "sql",
219            Self::Dart => "dart",
220            Self::Lua => "lua",
221            Self::Perl => "perl",
222            Self::Shell => "shell",
223            Self::Groovy => "groovy",
224            Self::Elixir => "elixir",
225            Self::R => "r",
226            Self::Haskell => "haskell",
227            Self::Html => "html",
228            Self::Svelte => "svelte",
229            Self::Vue => "vue",
230            Self::Zig => "zig",
231            Self::Terraform => "terraform",
232            Self::Puppet => "puppet",
233            Self::Pulumi => "pulumi",
234            Self::Http => "http",
235            Self::Plsql => "plsql",
236            Self::Apex => "apex",
237            Self::Abap => "abap",
238            Self::ServiceNow => "servicenow",
239            Self::Json => "json",
240        }
241    }
242
243    /// Additional accepted spellings beyond the canonical and short names.
244    ///
245    /// [`Self::from_id`] accepts the union of canonical, short, and these,
246    /// so an alias added here is immediately valid everywhere input is
247    /// parsed.
248    #[must_use]
249    pub const fn aliases(self) -> &'static [&'static str] {
250        match self {
251            Self::C => &[],
252            Self::Cpp => &["c++", "cplusplus", "cxx"],
253            Self::CSharp => &["c#", "cs"],
254            Self::Css => &[],
255            Self::JavaScript => &[],
256            Self::Python => &[],
257            Self::TypeScript => &[],
258            Self::Rust => &["rs"],
259            Self::Go => &["golang"],
260            Self::Java => &[],
261            Self::Ruby => &["rb"],
262            Self::Php => &[],
263            Self::Swift => &[],
264            Self::Kotlin => &["kt"],
265            Self::Scala => &[],
266            Self::Sql => &[],
267            Self::Dart => &[],
268            Self::Lua => &[],
269            Self::Perl => &["pl"],
270            Self::Shell => &["bash", "sh"],
271            Self::Groovy => &[],
272            Self::Elixir => &["ex", "exs"],
273            Self::R => &[],
274            Self::Haskell => &["hs"],
275            Self::Html => &["html5"],
276            Self::Svelte => &[],
277            Self::Vue => &[],
278            Self::Zig => &[],
279            Self::Terraform => &["hcl", "tf"],
280            Self::Puppet => &[],
281            Self::Pulumi => &[],
282            Self::Http => &[],
283            Self::Plsql => &["pl/sql", "oracle"],
284            Self::Apex => &["salesforce"],
285            Self::Abap => &[],
286            Self::ServiceNow => &["xanadu"],
287            Self::Json => &[],
288        }
289    }
290
291    /// Every spelling this language accepts on input, canonical first.
292    #[must_use]
293    pub fn accepted_names(self) -> Vec<&'static str> {
294        let mut names = vec![self.canonical_name()];
295        if self.short_name() != self.canonical_name() {
296            names.push(self.short_name());
297        }
298        names.extend_from_slice(self.aliases());
299        names
300    }
301
302    /// The canonical identifier of every language, in declaration order.
303    ///
304    /// Feeds the `lang` field registry and the documented value table so
305    /// neither can fall out of step with the enum.
306    #[must_use]
307    pub fn canonical_names() -> Vec<&'static str> {
308        Self::ALL.iter().map(|lang| lang.canonical_name()).collect()
309    }
310
311    /// Parse a language identifier or common alias into a `Language`.
312    ///
313    /// Accepts the canonical name, the short display name, and every alias,
314    /// after trimming and case folding. This is the only input parser: every
315    /// surface that accepts a user-supplied language routes through it, so
316    /// no surface can accept a different set of spellings than another
317    /// (issue #714).
318    #[must_use]
319    pub fn from_id(value: &str) -> Option<Self> {
320        let needle = value.trim().to_ascii_lowercase();
321        Self::ALL.iter().copied().find(|lang| {
322            lang.canonical_name() == needle
323                || lang.short_name() == needle
324                || lang.aliases().contains(&needle.as_str())
325        })
326    }
327}
328
329/// Universal node identifier with string interning for memory efficiency
330///
331/// Per AGENTS.md:149-151, uses `Arc<str>` to reduce memory usage for
332/// symbol-heavy data structures (saves 10-50 MB for typical repos).
333///
334/// # Examples
335///
336/// ```
337/// use sqry_core::graph::node::{NodeId, Language};
338/// use std::sync::Arc;
339///
340/// let node_id = NodeId::new(
341///     Language::Cpp,
342///     "src/main.cpp",
343///     "main"
344/// );
345///
346/// // Arc<str> makes cloning cheap (only refcount increment)
347/// let cloned = node_id.clone();
348/// assert_eq!(node_id, cloned);
349/// ```
350#[derive(Debug, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)]
351pub struct NodeId {
352    /// Language of origin
353    pub language: Language,
354    /// File path (interned via `Arc<str>`)
355    pub file: Arc<str>,
356    /// Qualified name (interned via `Arc<str>`)
357    /// Examples: "`std::vector::push_back`", "MyClass.process", "__main__"
358    pub qualified_name: Arc<str>,
359}
360
361impl NodeId {
362    /// Create a new `NodeId` with string interning
363    ///
364    /// Automatically interns strings via `Arc<str>` for memory efficiency.
365    ///
366    /// # Examples
367    ///
368    /// ```
369    /// use sqry_core::graph::node::{NodeId, Language};
370    ///
371    /// let id = NodeId::new(Language::Python, "api.py", "User.authenticate");
372    /// println!("{}", id); // "py:api.py:User.authenticate"
373    /// ```
374    pub fn new(language: Language, file: impl AsRef<str>, qualified_name: impl AsRef<str>) -> Self {
375        Self {
376            language,
377            file: Arc::from(file.as_ref()),
378            qualified_name: Arc::from(qualified_name.as_ref()),
379        }
380    }
381
382    /// Get the symbol name without namespace qualification
383    ///
384    /// # Examples
385    ///
386    /// ```
387    /// use sqry_core::graph::node::{NodeId, Language};
388    ///
389    /// let id = NodeId::new(Language::Cpp, "main.cpp", "std::vector::push_back");
390    /// assert_eq!(id.symbol_name(), "push_back");
391    /// ```
392    #[must_use]
393    pub fn symbol_name(&self) -> &str {
394        // Try C++ style first (::), then Python/Java style (.)
395        if let Some(name) = self.qualified_name.rsplit("::").next()
396            && name != self.qualified_name.as_ref()
397        {
398            return name;
399        }
400
401        if let Some(name) = self.qualified_name.rsplit('.').next() {
402            return name;
403        }
404
405        &self.qualified_name
406    }
407}
408
409impl fmt::Display for NodeId {
410    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
411        write!(f, "{}:{}:{}", self.language, self.file, self.qualified_name)
412    }
413}
414
415/// Source code span (line and column information)
416#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, Default, Serialize, Deserialize)]
417pub struct Span {
418    /// Starting position
419    pub start: Position,
420    /// Ending position
421    pub end: Position,
422}
423
424impl Span {
425    /// Create a new span
426    #[must_use]
427    pub fn new(start: Position, end: Position) -> Self {
428        Self { start, end }
429    }
430
431    /// Create a span from a tree-sitter node's real row and column.
432    ///
433    /// This is what a plugin should call when it has the node in hand, which
434    /// is nearly always. It takes the node by reference to match the private
435    /// extension traits that the cpp, kotlin and sql plugins had each already
436    /// written for themselves, which this replaces.
437    ///
438    /// Prefer it over [`Span::from_bytes`] without
439    /// exception: the two return the same type and look equally correct at a
440    /// call site, but `from_bytes` stores byte offsets in the line and column
441    /// fields, so anything built from it reports line 1 with a nonsense
442    /// column.
443    #[must_use]
444    pub fn from_node(node: &tree_sitter::Node<'_>) -> Self {
445        let start = node.start_position();
446        let end = node.end_position();
447        Self {
448            start: Position {
449                line: start.row,
450                column: start.column,
451            },
452            end: Position {
453                line: end.row,
454                column: end.column,
455            },
456        }
457    }
458
459    /// Create a span from raw byte offsets, WITHOUT resolving them to a line
460    /// and column.
461    ///
462    /// This is lossy and the loss is invisible: it stores the offsets in the
463    /// `line`/`column` fields and sets `line: 0`, which every display path
464    /// renders as line 1. A `Span` built this way is indistinguishable by type
465    /// from a correct one, which is how ten language plugins came to report
466    /// every declaration at line 1 with the byte offset as the column, found
467    /// by audit on 2026-08-22.
468    ///
469    /// Use [`Span::from_node`] when a node is available. This remains only for
470    /// call sites that genuinely hold nothing but offsets, such as a scope or
471    /// call-context tuple, and whose spans are not surfaced as symbol
472    /// positions.
473    ///
474    /// Deprecated so the rule is enforced by the compiler rather than by this
475    /// comment. Every language plugin that used to build declaration spans this
476    /// way reported line 1 with a byte offset in the column (issue #725), and a
477    /// new plugin reaching for it would reintroduce that. There are now ZERO
478    /// call sites: do not add `#[allow(deprecated)]` to create one. Use
479    /// [`Span::from_node`] when a node is in hand, or `LineIndex::span` when
480    /// only byte offsets are.
481    #[must_use]
482    #[deprecated(
483        since = "31.0.0",
484        note = "builds a line-1 span with the byte offset in the column; use Span::from_node, or LineIndex::span when only offsets are available"
485    )]
486    pub fn from_bytes(start: usize, end: usize) -> Self {
487        Self {
488            start: Position {
489                line: 0,
490                column: start,
491            },
492            end: Position {
493                line: 0,
494                column: end,
495            },
496        }
497    }
498}
499
500/// Position in source code (line and column)
501#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, Default, Serialize, Deserialize)]
502pub struct Position {
503    /// Line number (0-indexed)
504    pub line: usize,
505    /// Column number (0-indexed)
506    pub column: usize,
507}
508
509impl Position {
510    /// Create a new position
511    #[must_use]
512    pub fn new(line: usize, column: usize) -> Self {
513        Self { line, column }
514    }
515}
516
517/// Type of code entity
518#[derive(Debug, Clone, PartialEq)]
519pub enum NodeKind {
520    /// Function or method
521    Function {
522        /// Function parameters
523        params: Vec<Param>,
524        /// Return type (if known)
525        return_type: Option<Type>,
526        /// Whether the function is async
527        is_async: bool,
528    },
529    /// Class or struct
530    Class {
531        /// Base classes
532        bases: Vec<NodeId>,
533        /// Implemented interfaces
534        interfaces: Vec<NodeId>,
535    },
536    /// Module or namespace
537    Module {
538        /// Exported symbols
539        exports: Vec<NodeId>,
540    },
541    /// Variable, constant, or field
542    Variable {
543        /// Variable type (if known)
544        var_type: Option<Type>,
545    },
546}
547
548/// Function parameter
549#[derive(Debug, Clone, PartialEq)]
550pub struct Param {
551    /// Parameter name
552    pub name: String,
553    /// Parameter type (if known)
554    pub param_type: Option<Type>,
555}
556
557/// Type information (simplified for now)
558#[derive(Debug, Clone, PartialEq)]
559pub struct Type {
560    /// Type name
561    pub name: String,
562}
563
564/// Additional metadata for a node
565#[derive(Debug, Clone, Default)]
566pub struct NodeMetadata {
567    /// Visibility (public, private, etc.)
568    pub visibility: Option<String>,
569    /// Documentation string
570    pub doc_comment: Option<String>,
571    /// Attributes/decorators
572    pub attributes: Vec<String>,
573}
574
575/// A node in the code graph representing a code entity
576#[derive(Debug, Clone)]
577pub struct CodeNode {
578    /// Unique identifier
579    pub id: NodeId,
580    /// Node type (function, class, module, etc.)
581    pub kind: NodeKind,
582    /// Source location
583    pub span: Span,
584    /// Additional metadata
585    pub metadata: NodeMetadata,
586}
587
588#[cfg(test)]
589mod tests {
590    use super::*;
591
592    #[test]
593    fn test_node_id_creation() {
594        let id = NodeId::new(Language::Cpp, "src/main.cpp", "main");
595        assert_eq!(id.language, Language::Cpp);
596        assert_eq!(id.file.as_ref(), "src/main.cpp");
597        assert_eq!(id.qualified_name.as_ref(), "main");
598    }
599
600    #[test]
601    fn test_node_id_display() {
602        let id = NodeId::new(Language::Python, "api.py", "User.authenticate");
603        assert_eq!(id.to_string(), "py:api.py:User.authenticate");
604    }
605
606    #[test]
607    fn test_node_id_hash() {
608        use std::collections::HashSet;
609
610        let id1 = NodeId::new(Language::JavaScript, "api.js", "fetchUsers");
611        let id2 = NodeId::new(Language::JavaScript, "api.js", "fetchUsers");
612        let id3 = NodeId::new(Language::JavaScript, "api.js", "createUser");
613
614        let mut set = HashSet::new();
615        set.insert(id1.clone());
616        set.insert(id2.clone());
617        set.insert(id3.clone());
618
619        assert_eq!(set.len(), 2); // id1 and id2 are equal
620    }
621
622    #[test]
623    fn test_node_id_clone_cheap() {
624        let id1 = NodeId::new(Language::Cpp, "src/utils.cpp", "std::vector::push_back");
625        let id2 = id1.clone();
626
627        // Arc<str> means the underlying string is NOT copied
628        assert_eq!(Arc::as_ptr(&id1.file), Arc::as_ptr(&id2.file));
629        assert_eq!(
630            Arc::as_ptr(&id1.qualified_name),
631            Arc::as_ptr(&id2.qualified_name)
632        );
633    }
634
635    #[test]
636    fn test_symbol_name_extraction() {
637        let id1 = NodeId::new(Language::Cpp, "main.cpp", "std::vector::push_back");
638        assert_eq!(id1.symbol_name(), "push_back");
639
640        let id2 = NodeId::new(Language::Python, "api.py", "User.authenticate");
641        assert_eq!(id2.symbol_name(), "authenticate");
642
643        let id3 = NodeId::new(Language::JavaScript, "api.js", "fetchUsers");
644        assert_eq!(id3.symbol_name(), "fetchUsers");
645    }
646
647    #[test]
648    fn test_span_creation() {
649        let span = Span::new(Position::new(10, 0), Position::new(20, 1));
650
651        assert_eq!(span.start.line, 10);
652        assert_eq!(span.end.line, 20);
653    }
654
655    #[test]
656    fn every_variant_round_trips_through_from_id() {
657        // The invariant that makes the #714 bug class unrepresentable: every
658        // spelling a language publishes must parse back to that language.
659        for &lang in Language::ALL {
660            assert_eq!(
661                Language::from_id(lang.canonical_name()),
662                Some(lang),
663                "{} canonical name does not round-trip",
664                lang.canonical_name()
665            );
666            assert_eq!(
667                Language::from_id(lang.short_name()),
668                Some(lang),
669                "{} short name does not round-trip",
670                lang.short_name()
671            );
672            for alias in lang.aliases() {
673                assert_eq!(
674                    Language::from_id(alias),
675                    Some(lang),
676                    "alias {alias} does not round-trip"
677                );
678            }
679            // Case and surrounding whitespace must not change the answer.
680            assert_eq!(
681                Language::from_id(&format!("  {}  ", lang.canonical_name().to_uppercase())),
682                Some(lang)
683            );
684        }
685    }
686
687    #[test]
688    fn language_all_is_complete_and_unique() {
689        assert_eq!(Language::ALL.len(), 37);
690        let names = Language::canonical_names();
691        let unique: std::collections::HashSet<_> = names.iter().collect();
692        assert_eq!(unique.len(), names.len(), "canonical names must be unique");
693    }
694
695    #[test]
696    fn canonical_and_short_names_are_pinned() {
697        // Guards both directions of the split that caused #714. The canonical
698        // name is what every predicate and machine-facing filter speaks; the
699        // short name is persisted in manifest confidence keys and textual
700        // NodeIds, so neither may drift silently.
701        assert_eq!(Language::TypeScript.canonical_name(), "typescript");
702        assert_eq!(Language::TypeScript.short_name(), "ts");
703        assert_eq!(Language::JavaScript.canonical_name(), "javascript");
704        assert_eq!(Language::JavaScript.short_name(), "js");
705        assert_eq!(Language::Python.canonical_name(), "python");
706        assert_eq!(Language::Python.short_name(), "py");
707        // Display is the short name, unchanged: it is a persisted contract.
708        assert_eq!(Language::TypeScript.to_string(), "ts");
709        assert_eq!(Language::JavaScript.to_string(), "js");
710        assert_eq!(Language::Python.to_string(), "py");
711    }
712
713    #[test]
714    fn unknown_is_not_a_language() {
715        // Several surfaces map a language-less file to the literal "unknown".
716        // It must never become a Language variant, or those surfaces would
717        // start conflating "no language" with a real one.
718        assert_eq!(Language::from_id("unknown"), None);
719        assert_eq!(Language::from_id("bogus"), None);
720        assert_eq!(Language::from_id(""), None);
721        // Plugin ids are not language ids.
722        assert_eq!(Language::from_id("servicenow-xanadu"), None);
723        assert_eq!(Language::from_id("servicenow-xml"), None);
724    }
725
726    #[test]
727    fn test_language_display() {
728        assert_eq!(Language::Cpp.to_string(), "cpp");
729        assert_eq!(Language::JavaScript.to_string(), "js");
730        assert_eq!(Language::Python.to_string(), "py");
731        assert_eq!(Language::Ruby.to_string(), "ruby");
732        assert_eq!(Language::Php.to_string(), "php");
733        assert_eq!(Language::Swift.to_string(), "swift");
734        assert_eq!(Language::Kotlin.to_string(), "kotlin");
735        assert_eq!(Language::Scala.to_string(), "scala");
736        assert_eq!(Language::Http.to_string(), "http");
737    }
738
739    #[test]
740    fn test_language_from_id() {
741        assert_eq!(Language::from_id("javascript"), Some(Language::JavaScript));
742        assert_eq!(Language::from_id("js"), Some(Language::JavaScript));
743        assert_eq!(Language::from_id("c#"), Some(Language::CSharp));
744        assert_eq!(Language::from_id("rb"), Some(Language::Ruby));
745        assert_eq!(Language::from_id("json"), Some(Language::Json));
746        assert_eq!(Language::from_id("unknown"), None);
747    }
748}