@@ -316,6 +316,214 @@ fn collect_expression_references(
316316 }
317317}
318318
319+ /// Helper struct to hold term information for validation
320+ #[ derive( Debug , Clone ) ]
321+ struct TermInfo {
322+ attributes : HashSet < String > ,
323+ }
324+
325+ /// Collect all term definitions from a resource
326+ fn collect_terms_from_resource ( resource : & FluentResource ) -> HashMap < String , TermInfo > {
327+ use fluent_syntax:: ast;
328+ let mut terms = HashMap :: new ( ) ;
329+
330+ for entry in resource. entries ( ) {
331+ if let ast:: Entry :: Term ( term) = entry {
332+ let term_id = format ! ( "-{}" , term. id. name) ;
333+ let attributes: HashSet < String > = term
334+ . attributes
335+ . iter ( )
336+ . map ( |attr| attr. id . name . to_string ( ) )
337+ . collect ( ) ;
338+ terms. insert ( term_id, TermInfo { attributes } ) ;
339+ }
340+ }
341+
342+ terms
343+ }
344+
345+ /// Helper function to check all references in a resource against the bundle and available terms
346+ fn check_references (
347+ resource : & FluentResource ,
348+ bundle : & FluentBundle < FluentResource > ,
349+ available_terms : & HashMap < String , TermInfo > ,
350+ ) -> Vec < ValidationError > {
351+ use fluent_syntax:: ast;
352+ let mut errors = Vec :: new ( ) ;
353+
354+ for entry in resource. entries ( ) {
355+ match entry {
356+ ast:: Entry :: Message ( msg) => {
357+ let msg_id = msg. id . name . to_string ( ) ;
358+ check_pattern_references ( bundle, & msg. value , & msg_id, available_terms, & mut errors) ;
359+ for attr in & msg. attributes {
360+ check_pattern_references (
361+ bundle,
362+ & Some ( attr. value . clone ( ) ) ,
363+ & msg_id,
364+ available_terms,
365+ & mut errors,
366+ ) ;
367+ }
368+ }
369+ ast:: Entry :: Term ( term) => {
370+ let term_id = format ! ( "-{}" , term. id. name) ;
371+ check_pattern_references (
372+ bundle,
373+ & Some ( term. value . clone ( ) ) ,
374+ & term_id,
375+ available_terms,
376+ & mut errors,
377+ ) ;
378+ for attr in & term. attributes {
379+ check_pattern_references (
380+ bundle,
381+ & Some ( attr. value . clone ( ) ) ,
382+ & term_id,
383+ available_terms,
384+ & mut errors,
385+ ) ;
386+ }
387+ }
388+ _ => { }
389+ }
390+ }
391+
392+ errors
393+ }
394+
395+ fn check_pattern_references (
396+ bundle : & FluentBundle < FluentResource > ,
397+ pattern : & Option < fluent_syntax:: ast:: Pattern < & str > > ,
398+ current_msg_id : & str ,
399+ available_terms : & HashMap < String , TermInfo > ,
400+ errors : & mut Vec < ValidationError > ,
401+ ) {
402+ use fluent_syntax:: ast;
403+
404+ if let Some ( pattern) = pattern {
405+ for element in & pattern. elements {
406+ if let ast:: PatternElement :: Placeable { expression } = element {
407+ check_expression_references (
408+ bundle,
409+ expression,
410+ current_msg_id,
411+ available_terms,
412+ errors,
413+ ) ;
414+ }
415+ }
416+ }
417+ }
418+
419+ fn check_expression_references (
420+ bundle : & FluentBundle < FluentResource > ,
421+ expression : & fluent_syntax:: ast:: Expression < & str > ,
422+ current_msg_id : & str ,
423+ available_terms : & HashMap < String , TermInfo > ,
424+ errors : & mut Vec < ValidationError > ,
425+ ) {
426+ use fluent_syntax:: ast;
427+
428+ match expression {
429+ ast:: Expression :: Inline ( inline) => {
430+ match inline {
431+ ast:: InlineExpression :: MessageReference { id, attribute } => {
432+ if !bundle. has_message ( id. name ) {
433+ errors. push ( ValidationError {
434+ error_type : "UnknownMessage" . to_string ( ) ,
435+ message : format ! ( "Unknown message: {}" , id. name) ,
436+ message_id : Some ( current_msg_id. to_string ( ) ) ,
437+ reference : Some ( id. name . to_string ( ) ) ,
438+ } ) ;
439+ } else if let Some ( attr) = attribute {
440+ if let Some ( msg) = bundle. get_message ( id. name ) {
441+ if msg. get_attribute ( attr. name ) . is_none ( ) {
442+ errors. push ( ValidationError {
443+ error_type : "UnknownAttribute" . to_string ( ) ,
444+ message : format ! (
445+ "Unknown attribute: {}.{}" ,
446+ id. name, attr. name
447+ ) ,
448+ message_id : Some ( current_msg_id. to_string ( ) ) ,
449+ reference : Some ( format ! ( "{}.{}" , id. name, attr. name) ) ,
450+ } ) ;
451+ }
452+ }
453+ }
454+ }
455+ ast:: InlineExpression :: TermReference {
456+ id,
457+ attribute,
458+ arguments,
459+ } => {
460+ let term_id = format ! ( "-{}" , id. name) ;
461+
462+ // Validate that terms don't receive positional arguments
463+ // Per Fluent spec, positional arguments to terms are ignored, so we warn about them
464+ if let Some ( args) = arguments {
465+ if !args. positional . is_empty ( ) {
466+ errors. push ( ValidationError {
467+ error_type : "IgnoredPositionalArgument" . to_string ( ) ,
468+ message : format ! (
469+ "Positional arguments passed to term -{} are ignored. Use named arguments instead." ,
470+ id. name
471+ ) ,
472+ message_id : Some ( current_msg_id. to_string ( ) ) ,
473+ reference : Some ( term_id. clone ( ) ) ,
474+ } ) ;
475+ }
476+ }
477+
478+ // Check against available_terms instead of bundle.has_message
479+ if !available_terms. contains_key ( & term_id) {
480+ errors. push ( ValidationError {
481+ error_type : "UnknownTerm" . to_string ( ) ,
482+ message : format ! ( "Unknown term: -{}" , id. name) ,
483+ message_id : Some ( current_msg_id. to_string ( ) ) ,
484+ reference : Some ( term_id) ,
485+ } ) ;
486+ } else if let Some ( attr) = attribute {
487+ // Check term attributes from available_terms instead of bundle.get_message
488+ if let Some ( term_info) = available_terms. get ( & term_id) {
489+ if !term_info. attributes . contains ( attr. name ) {
490+ errors. push ( ValidationError {
491+ error_type : "UnknownAttribute" . to_string ( ) ,
492+ message : format ! (
493+ "Unknown attribute on term: -{}.{}" ,
494+ id. name, attr. name
495+ ) ,
496+ message_id : Some ( current_msg_id. to_string ( ) ) ,
497+ reference : Some ( format ! ( "-{}.{}" , id. name, attr. name) ) ,
498+ } ) ;
499+ }
500+ }
501+ }
502+ }
503+ _ => { }
504+ }
505+ }
506+ ast:: Expression :: Select { selector, variants } => {
507+ check_expression_references (
508+ bundle,
509+ & ast:: Expression :: Inline ( ( * selector) . clone ( ) ) ,
510+ current_msg_id,
511+ available_terms,
512+ errors,
513+ ) ;
514+ for variant in variants {
515+ check_pattern_references (
516+ bundle,
517+ & Some ( variant. value . clone ( ) ) ,
518+ current_msg_id,
519+ available_terms,
520+ errors,
521+ ) ;
522+ }
523+ }
524+ }
525+ }
526+
319527#[ pymodule]
320528mod rustfluent {
321529 use super :: * ;
0 commit comments